@operato/twin-kernel 0.3.0 → 0.4.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.
- package/dist/capacity.d.ts +99 -0
- package/dist/capacity.js +172 -0
- package/dist/contract.d.ts +512 -23
- package/dist/contract.js +337 -13
- package/dist/divergence.js +5 -2
- package/dist/domain-definition.d.ts +45 -0
- package/dist/epcis.d.ts +12 -0
- package/dist/epcis.js +12 -0
- package/dist/event-journal.d.ts +30 -0
- package/dist/event-journal.js +26 -0
- package/dist/flow-engine.d.ts +270 -7
- package/dist/flow-engine.js +727 -52
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/job-response.d.ts +59 -0
- package/dist/job-response.js +56 -0
- package/dist/kernel.d.ts +5 -0
- package/dist/kernel.js +25 -5
- package/dist/mes-kernel.d.ts +28 -0
- package/dist/mes-kernel.js +58 -4
- package/dist/observed-reducer.d.ts +61 -0
- package/dist/observed-reducer.js +214 -17
- package/dist/vocabulary.js +6 -0
- package/dist/yms-kernel.js +26 -5
- package/dist-cjs/index.cjs +1240 -85
- package/package.json +1 -1
package/dist-cjs/index.cjs
CHANGED
|
@@ -25,6 +25,7 @@ __export(index_exports, {
|
|
|
25
25
|
BTT_PRODORDER: () => BTT_PRODORDER,
|
|
26
26
|
CAPABILITIES: () => CAPABILITIES,
|
|
27
27
|
CAPABILITY_KEYS: () => CAPABILITY_KEYS,
|
|
28
|
+
CBV_BIZSTEP: () => CBV_BIZSTEP,
|
|
28
29
|
CMD: () => CMD,
|
|
29
30
|
DISP: () => DISP,
|
|
30
31
|
DOMAIN_CATALOG: () => DOMAIN_CATALOG,
|
|
@@ -36,6 +37,7 @@ __export(index_exports, {
|
|
|
36
37
|
GUARD_PRAGMA: () => GUARD_PRAGMA,
|
|
37
38
|
ILMD_ATTR: () => ILMD_ATTR,
|
|
38
39
|
LOCATION_SATURATION_NEAR: () => LOCATION_SATURATION_NEAR,
|
|
40
|
+
MATERIAL_PROPERTY: () => MATERIAL_PROPERTY,
|
|
39
41
|
MES_BIZSTEP: () => MES_BIZSTEP,
|
|
40
42
|
MES_LOCATION_TYPES: () => MES_LOCATION_TYPES,
|
|
41
43
|
MES_PART_GTINS: () => MES_PART_GTINS,
|
|
@@ -46,6 +48,7 @@ __export(index_exports, {
|
|
|
46
48
|
OP_EVENT: () => OP_EVENT,
|
|
47
49
|
OP_PARAM: () => OP_PARAM,
|
|
48
50
|
ObservedReducer: () => ObservedReducer,
|
|
51
|
+
PRIORITY_UNSET: () => PRIORITY_UNSET,
|
|
49
52
|
RETIRED_VOCABULARY: () => RETIRED_VOCABULARY,
|
|
50
53
|
StateProjector: () => ObservedReducer,
|
|
51
54
|
TwinHistory: () => TwinHistory,
|
|
@@ -60,43 +63,63 @@ __export(index_exports, {
|
|
|
60
63
|
YMS_LOCATION_TYPES: () => YMS_LOCATION_TYPES,
|
|
61
64
|
YMS_TYPES: () => YMS_TYPES,
|
|
62
65
|
YmsKernel: () => YmsKernel,
|
|
66
|
+
activeShiftAt: () => activeShiftAt,
|
|
67
|
+
activeShiftOf: () => activeShiftOf,
|
|
63
68
|
aggregationEvent: () => aggregationEvent,
|
|
69
|
+
analyzeCapacity: () => analyzeCapacity,
|
|
64
70
|
capabilitiesForType: () => capabilitiesForType,
|
|
65
71
|
classClosure: () => classClosure,
|
|
66
72
|
compareStates: () => compareStates,
|
|
67
73
|
computeOee: () => computeOee,
|
|
68
74
|
constantDuration: () => constantDuration,
|
|
75
|
+
conversionFactorOf: () => conversionFactorOf,
|
|
69
76
|
counterfactualAt: () => counterfactualAt,
|
|
70
77
|
deriveAttentions: () => deriveAttentions,
|
|
78
|
+
dueStatusOf: () => dueStatusOf,
|
|
79
|
+
effectivityAt: () => effectivityAt,
|
|
71
80
|
fefoPolicy: () => fefoPolicy,
|
|
72
81
|
firstFitPolicy: () => firstFitPolicy,
|
|
82
|
+
foldJobResponses: () => foldJobResponses,
|
|
73
83
|
foldTaskRecords: () => foldTaskRecords,
|
|
74
84
|
gdtiUri: () => gdtiUri,
|
|
75
85
|
graiUri: () => graiUri,
|
|
76
86
|
hierarchyOf: () => hierarchyOf,
|
|
87
|
+
inWorkCalendar: () => inWorkCalendar,
|
|
88
|
+
inWorkCalendarAt: () => inWorkCalendarAt,
|
|
77
89
|
ingest: () => ingest,
|
|
78
90
|
isEquipmentLevel: () => isEquipmentLevel,
|
|
91
|
+
isoDurationHours: () => isoDurationHours,
|
|
92
|
+
itemKeyOf: () => itemKeyOf,
|
|
79
93
|
lgtinClass: () => lgtinClass,
|
|
80
94
|
locationStatusOf: () => locationStatusOf,
|
|
81
95
|
mapRecord: () => mapRecord,
|
|
96
|
+
minuteOfDayAt: () => minuteOfDayAt,
|
|
82
97
|
monteCarloForecast: () => monteCarloForecast,
|
|
83
98
|
objectEvent: () => objectEvent,
|
|
99
|
+
offCalendarAt: () => offCalendarAt,
|
|
100
|
+
offCalendarReasonAt: () => offCalendarReasonAt,
|
|
84
101
|
parseEpc: () => parseEpc,
|
|
85
102
|
parseIsoDuration: () => parseIsoDuration,
|
|
86
103
|
partialFitPolicy: () => partialFitPolicy,
|
|
104
|
+
priorityRank: () => priorityRank,
|
|
105
|
+
quantityIn: () => quantityIn,
|
|
87
106
|
readBoardAssets: () => readBoardAssets,
|
|
88
107
|
readBoardEquipment: () => readBoardEquipment,
|
|
89
108
|
readBoardLocations: () => readBoardLocations,
|
|
90
109
|
replay: () => replay,
|
|
110
|
+
replaySegments: () => replaySegments,
|
|
91
111
|
retiredVocabularyIn: () => retiredVocabularyIn,
|
|
92
112
|
sgtinClass: () => sgtinClass,
|
|
93
113
|
sgtinUri: () => sgtinUri,
|
|
94
114
|
ssccUri: () => ssccUri,
|
|
95
115
|
stateFieldsOf: () => stateFieldsOf,
|
|
116
|
+
subLotIdOf: () => subLotIdOf,
|
|
96
117
|
transactionEvent: () => transactionEvent,
|
|
97
118
|
transformationEvent: () => transformationEvent,
|
|
98
119
|
validateDomainDefinition: () => validateDomainDefinition,
|
|
99
|
-
validateEpcisEvent: () => validateEpcisEvent
|
|
120
|
+
validateEpcisEvent: () => validateEpcisEvent,
|
|
121
|
+
weekdayAt: () => weekdayAt,
|
|
122
|
+
workingTimeOfWeek: () => workingTimeOfWeek
|
|
100
123
|
});
|
|
101
124
|
module.exports = __toCommonJS(index_exports);
|
|
102
125
|
|
|
@@ -185,17 +208,19 @@ function hierarchyOf(s) {
|
|
|
185
208
|
}
|
|
186
209
|
};
|
|
187
210
|
}
|
|
211
|
+
function effectivityAt(p, at) {
|
|
212
|
+
if (!p || !at) return void 0;
|
|
213
|
+
const atMs = Date.parse(at);
|
|
214
|
+
if (!Number.isFinite(atMs)) return void 0;
|
|
215
|
+
const from = p.effectiveStart ? Date.parse(p.effectiveStart) : NaN;
|
|
216
|
+
if (Number.isFinite(from) && atMs < from) return "not-yet";
|
|
217
|
+
const to = p.effectiveEnd ? Date.parse(p.effectiveEnd) : NaN;
|
|
218
|
+
if (Number.isFinite(to) && atMs > to) return "expired";
|
|
219
|
+
return void 0;
|
|
220
|
+
}
|
|
188
221
|
function classClosure(directIds, defs, at) {
|
|
189
222
|
const byId = new Map((defs ?? []).map((d) => [d.id, d]));
|
|
190
|
-
const
|
|
191
|
-
const inWindow = (d) => {
|
|
192
|
-
if (!d || !Number.isFinite(atMs)) return true;
|
|
193
|
-
const from = d.effectiveStart ? Date.parse(d.effectiveStart) : NaN;
|
|
194
|
-
const to = d.effectiveEnd ? Date.parse(d.effectiveEnd) : NaN;
|
|
195
|
-
if (Number.isFinite(from) && atMs < from) return false;
|
|
196
|
-
if (Number.isFinite(to) && atMs > to) return false;
|
|
197
|
-
return true;
|
|
198
|
-
};
|
|
223
|
+
const inWindow = (d) => !d || effectivityAt(d, at) === void 0;
|
|
199
224
|
const out = /* @__PURE__ */ new Set();
|
|
200
225
|
const stack = [...directIds ?? []];
|
|
201
226
|
while (stack.length) {
|
|
@@ -208,6 +233,155 @@ function classClosure(directIds, defs, at) {
|
|
|
208
233
|
}
|
|
209
234
|
return out;
|
|
210
235
|
}
|
|
236
|
+
var PRIORITY_UNSET = Number.POSITIVE_INFINITY;
|
|
237
|
+
function priorityRank(p) {
|
|
238
|
+
return typeof p === "number" && Number.isFinite(p) ? p : PRIORITY_UNSET;
|
|
239
|
+
}
|
|
240
|
+
function dueStatusOf(x, nowIso) {
|
|
241
|
+
if (!x.endTime || !nowIso) return void 0;
|
|
242
|
+
const due = Date.parse(x.endTime);
|
|
243
|
+
const now = Date.parse(nowIso);
|
|
244
|
+
if (!Number.isFinite(due) || !Number.isFinite(now)) return void 0;
|
|
245
|
+
return now > due ? "late" : "on-time";
|
|
246
|
+
}
|
|
247
|
+
function subLotIdOf(classUri, location) {
|
|
248
|
+
return `${classUri}@${location}`;
|
|
249
|
+
}
|
|
250
|
+
function itemKeyOf(item) {
|
|
251
|
+
return item.subLotId ?? item.epc;
|
|
252
|
+
}
|
|
253
|
+
function quantityIn(item, uom, definitions) {
|
|
254
|
+
const hit = item.quantities?.find((q) => (q.uom ?? void 0) === (uom ?? void 0));
|
|
255
|
+
if (hit) return hit.value;
|
|
256
|
+
if ((item.uom ?? void 0) === (uom ?? void 0)) return item.qty;
|
|
257
|
+
return convertQuantity(item, uom, definitions);
|
|
258
|
+
}
|
|
259
|
+
var MATERIAL_PROPERTY = {
|
|
260
|
+
/** 기준 단위 하나당 이 단위의 양. `uom` 이 대상 단위. */
|
|
261
|
+
perBaseUnit: "perBaseUnit"
|
|
262
|
+
};
|
|
263
|
+
function conversionFactorOf(def, uom) {
|
|
264
|
+
if (!def || !uom) return void 0;
|
|
265
|
+
for (const p of def.properties ?? []) {
|
|
266
|
+
if (p.id !== MATERIAL_PROPERTY.perBaseUnit || p.uom !== uom) continue;
|
|
267
|
+
const n = typeof p.value === "number" ? p.value : Number(p.value);
|
|
268
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
269
|
+
}
|
|
270
|
+
return void 0;
|
|
271
|
+
}
|
|
272
|
+
function convertQuantity(item, uom, definitions) {
|
|
273
|
+
if (!definitions || !uom) return void 0;
|
|
274
|
+
if (typeof item.qty !== "number" || !Number.isFinite(item.qty)) return void 0;
|
|
275
|
+
if (item.uom) return void 0;
|
|
276
|
+
const def = definitions.get(item.definitionId ?? item.gtin ?? "");
|
|
277
|
+
const factor = conversionFactorOf(def, uom);
|
|
278
|
+
return factor === void 0 ? void 0 : item.qty * factor;
|
|
279
|
+
}
|
|
280
|
+
function minutesOfDay(hhmm) {
|
|
281
|
+
const m = /^(\d{1,2}):(\d{2})$/.exec(hhmm ?? "");
|
|
282
|
+
if (!m) return void 0;
|
|
283
|
+
const h = Number(m[1]);
|
|
284
|
+
const mi = Number(m[2]);
|
|
285
|
+
if (h > 23 || mi > 59) return void 0;
|
|
286
|
+
return h * 60 + mi;
|
|
287
|
+
}
|
|
288
|
+
var isAbsolute = (e) => !!(e.startDateTime || e.finishDateTime);
|
|
289
|
+
var needsDate = (e) => isAbsolute(e) || !!e.daysOfWeek?.length;
|
|
290
|
+
function onDeclaredDay(e, minuteOfDay, weekday) {
|
|
291
|
+
if (!e.daysOfWeek?.length) return true;
|
|
292
|
+
const from = minutesOfDay(e.fromTime);
|
|
293
|
+
const to = minutesOfDay(e.toTime);
|
|
294
|
+
const crosses = from !== void 0 && to !== void 0 && from > to;
|
|
295
|
+
const startedDay = crosses && minuteOfDay < to ? (weekday + 6) % 7 : weekday;
|
|
296
|
+
return e.daysOfWeek.includes(startedDay);
|
|
297
|
+
}
|
|
298
|
+
function weekdayAt(ms2, utcOffsetMinutes) {
|
|
299
|
+
return new Date(ms2 + (utcOffsetMinutes ?? 0) * 6e4).getUTCDay();
|
|
300
|
+
}
|
|
301
|
+
function coversInstant(e, atMs) {
|
|
302
|
+
const from = e.startDateTime ? Date.parse(e.startDateTime) : NaN;
|
|
303
|
+
const to = e.finishDateTime ? Date.parse(e.finishDateTime) : NaN;
|
|
304
|
+
if (!Number.isFinite(from) && !Number.isFinite(to)) return false;
|
|
305
|
+
if (Number.isFinite(from) && atMs < from) return false;
|
|
306
|
+
if (Number.isFinite(to) && atMs >= to) return false;
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
function coversMinute(e, minuteOfDay) {
|
|
310
|
+
const from = minutesOfDay(e.fromTime);
|
|
311
|
+
const to = minutesOfDay(e.toTime);
|
|
312
|
+
if (from === void 0 || to === void 0) return false;
|
|
313
|
+
return from <= to ? minuteOfDay >= from && minuteOfDay < to : minuteOfDay >= from || minuteOfDay < to;
|
|
314
|
+
}
|
|
315
|
+
function inWorkCalendar(entries, minuteOfDay) {
|
|
316
|
+
return judgeCalendar(entries, (e) => coversMinute(e, minuteOfDay), false);
|
|
317
|
+
}
|
|
318
|
+
function inWorkCalendarAt(entries, atMs, utcOffsetMinutes) {
|
|
319
|
+
const minute = minuteOfDayAt(atMs, utcOffsetMinutes);
|
|
320
|
+
const day = weekdayAt(atMs, utcOffsetMinutes);
|
|
321
|
+
const covers = (e) => {
|
|
322
|
+
if (isAbsolute(e)) return coversInstant(e, atMs);
|
|
323
|
+
if (!coversMinute(e, minute)) return false;
|
|
324
|
+
return onDeclaredDay(e, minute, day);
|
|
325
|
+
};
|
|
326
|
+
return judgeCalendar(entries, covers, true);
|
|
327
|
+
}
|
|
328
|
+
function judgeCalendar(entries, covers, withAbsolute) {
|
|
329
|
+
if (!entries?.length) return true;
|
|
330
|
+
const usable = withAbsolute ? entries : entries.filter((e) => !needsDate(e));
|
|
331
|
+
if (!usable.length) return true;
|
|
332
|
+
const working = usable.filter((e) => (e.entryType ?? "working") === "working");
|
|
333
|
+
const off = usable.filter((e) => e.entryType === "non-working");
|
|
334
|
+
if (off.some(covers)) return false;
|
|
335
|
+
if (!working.length) return true;
|
|
336
|
+
return working.some(covers);
|
|
337
|
+
}
|
|
338
|
+
function activeShiftOf(entries, minuteOfDay) {
|
|
339
|
+
if (!entries?.length) return void 0;
|
|
340
|
+
if (!inWorkCalendar(entries, minuteOfDay)) return void 0;
|
|
341
|
+
for (const e of entries) {
|
|
342
|
+
if (needsDate(e) || (e.entryType ?? "working") !== "working" || !e.id) continue;
|
|
343
|
+
if (coversMinute(e, minuteOfDay)) return e.id;
|
|
344
|
+
}
|
|
345
|
+
return void 0;
|
|
346
|
+
}
|
|
347
|
+
function activeShiftAt(entries, atMs, utcOffsetMinutes) {
|
|
348
|
+
if (!entries?.length) return void 0;
|
|
349
|
+
if (!inWorkCalendarAt(entries, atMs, utcOffsetMinutes)) return void 0;
|
|
350
|
+
const minute = minuteOfDayAt(atMs, utcOffsetMinutes);
|
|
351
|
+
const day = weekdayAt(atMs, utcOffsetMinutes);
|
|
352
|
+
for (const e of entries) {
|
|
353
|
+
if (isAbsolute(e) || (e.entryType ?? "working") !== "working" || !e.id) continue;
|
|
354
|
+
if (!coversMinute(e, minute)) continue;
|
|
355
|
+
if (!onDeclaredDay(e, minute, day)) continue;
|
|
356
|
+
return e.id;
|
|
357
|
+
}
|
|
358
|
+
return void 0;
|
|
359
|
+
}
|
|
360
|
+
function minuteOfDayAt(ms2, utcOffsetMinutes) {
|
|
361
|
+
const d = new Date(ms2 + (utcOffsetMinutes ?? 0) * 6e4);
|
|
362
|
+
return d.getUTCHours() * 60 + d.getUTCMinutes();
|
|
363
|
+
}
|
|
364
|
+
function offCalendarReasonAt(r, ms2, utcOffsetMinutes) {
|
|
365
|
+
if (!offCalendarAt(r, ms2, utcOffsetMinutes)) return void 0;
|
|
366
|
+
const entries = r.workCalendar;
|
|
367
|
+
if (!entries?.length) return "off-hours";
|
|
368
|
+
const minute = minuteOfDayAt(ms2, utcOffsetMinutes);
|
|
369
|
+
const day = weekdayAt(ms2, utcOffsetMinutes);
|
|
370
|
+
for (const e of entries) {
|
|
371
|
+
if (e.entryType !== "non-working") continue;
|
|
372
|
+
const covered = isAbsolute(e) ? coversInstant(e, ms2) : coversMinute(e, minute) && onDeclaredDay(e, minute, day);
|
|
373
|
+
if (covered) return "non-working";
|
|
374
|
+
}
|
|
375
|
+
return "off-hours";
|
|
376
|
+
}
|
|
377
|
+
function offCalendarAt(r, ms2, utcOffsetMinutes) {
|
|
378
|
+
const minute = minuteOfDayAt(ms2, utcOffsetMinutes);
|
|
379
|
+
if (r.workCalendar?.length) return !inWorkCalendarAt(r.workCalendar, ms2, utcOffsetMinutes);
|
|
380
|
+
const w = r.window;
|
|
381
|
+
if (!w) return false;
|
|
382
|
+
const h = Math.floor(minute / 60);
|
|
383
|
+
return !(w.startHour <= w.endHour ? h >= w.startHour && h < w.endHour : h >= w.startHour || h < w.endHour);
|
|
384
|
+
}
|
|
211
385
|
var OP_EVENT = {
|
|
212
386
|
task: "task.status",
|
|
213
387
|
equipment: "equipment.status",
|
|
@@ -245,7 +419,7 @@ function readBoardLocations(def) {
|
|
|
245
419
|
}
|
|
246
420
|
function readBoardEquipment(def) {
|
|
247
421
|
const d = def;
|
|
248
|
-
const list = d.equipment ?? d.equipmentList ?? [];
|
|
422
|
+
const list = d.equipment ?? d.equipmentList ?? d.movers ?? [];
|
|
249
423
|
return list.map((e) => normalizeHomeLocation(e));
|
|
250
424
|
}
|
|
251
425
|
function normalizeHomeLocation(entry) {
|
|
@@ -326,8 +500,8 @@ function diffBy(predicted, actual, idOf, valOf) {
|
|
|
326
500
|
return out.sort((x, y) => x.id.localeCompare(y.id));
|
|
327
501
|
}
|
|
328
502
|
function compareStates(predicted, actual) {
|
|
329
|
-
const itemLocation = diffBy(predicted.items, actual.items,
|
|
330
|
-
const itemDisposition = diffBy(predicted.items, actual.items,
|
|
503
|
+
const itemLocation = diffBy(predicted.items, actual.items, itemKeyOf, (i) => i.location);
|
|
504
|
+
const itemDisposition = diffBy(predicted.items, actual.items, itemKeyOf, (i) => i.disposition);
|
|
331
505
|
const locationOccupancy = diffBy(predicted.locations, actual.locations, (n) => n.id, (n) => n.occupancy);
|
|
332
506
|
const orderStatus = diffBy(predicted.orders, actual.orders, (o) => o.id, (o) => o.status);
|
|
333
507
|
const hasDrift = itemLocation.length > 0 || itemDisposition.length > 0 || locationOccupancy.length > 0 || orderStatus.length > 0;
|
|
@@ -454,6 +628,12 @@ var DISP = {
|
|
|
454
628
|
non_sellable: "urn:epcglobal:cbv:disp:non_sellable_other"
|
|
455
629
|
// 불량/scrap
|
|
456
630
|
};
|
|
631
|
+
var CBV_BIZSTEP = {
|
|
632
|
+
/** 공정에 자재가 들어갔다 — ISA-95 `MaterialUse: Consumed`. */
|
|
633
|
+
consuming: "urn:epcglobal:cbv:bizstep:consuming",
|
|
634
|
+
/** 새 물품이 생겨 계보가 시작된다 — ISA-95 `MaterialUse: Produced`. */
|
|
635
|
+
commissioning: "urn:epcglobal:cbv:bizstep:commissioning"
|
|
636
|
+
};
|
|
457
637
|
function ssccUri(companyPrefix, serial) {
|
|
458
638
|
return `urn:epc:id:sscc:${companyPrefix}.${String(serial).padStart(10, "0")}`;
|
|
459
639
|
}
|
|
@@ -659,6 +839,12 @@ function validateEpcisEvent(e) {
|
|
|
659
839
|
|
|
660
840
|
// src/observed-reducer.ts
|
|
661
841
|
var UNKNOWN_TYPE = "unknown";
|
|
842
|
+
function effectiveOf(r) {
|
|
843
|
+
return {
|
|
844
|
+
...r?.effectiveStart ? { effectiveStart: r.effectiveStart } : {},
|
|
845
|
+
...r?.effectiveEnd ? { effectiveEnd: r.effectiveEnd } : {}
|
|
846
|
+
};
|
|
847
|
+
}
|
|
662
848
|
var ObservedReducer = class {
|
|
663
849
|
/** 로케이션 마스터 — 출처를 함께 들고 있다(마스터가 말한 자리 vs 관측으로 알게 된 자리). */
|
|
664
850
|
master = /* @__PURE__ */ new Map();
|
|
@@ -675,11 +861,86 @@ var ObservedReducer = class {
|
|
|
675
861
|
revision = 0;
|
|
676
862
|
/** 받은 정정 선언 — 상태에 반영하지 않되 **버리지도 않는다**(소비처가 볼 수 있게). */
|
|
677
863
|
corrections = [];
|
|
864
|
+
/**
|
|
865
|
+
* 자원별 **교대 선언** — 미러가 "지금 근무 중인가" 를 스스로 판정하기 위한 재료.
|
|
866
|
+
*
|
|
867
|
+
* 상태(`EquipmentState`)에 넣지 않는다: 시뮬 스냅샷도 이것을 내보내지 않으므로 넣으면 두 구동이
|
|
868
|
+
* 갈라진다(적합성 하네스가 `mirrorOnly` 로 잡는다). 이것은 **판정의 입력**이고, 나가는 것은 판정뿐이다.
|
|
869
|
+
*/
|
|
870
|
+
shifts = /* @__PURE__ */ new Map();
|
|
871
|
+
/** 시각 해석 기준(보드 선언) — 없으면 UTC. 캘린더의 `HH:MM` 이 어느 기준인지 정한다. */
|
|
872
|
+
utcOffsetMinutes;
|
|
678
873
|
constructor(board) {
|
|
874
|
+
this.utcOffsetMinutes = board.utcOffsetMinutes;
|
|
679
875
|
for (const n of readBoardLocations(board)) this.master.set(n.id, { id: n.id, type: n.type, capacity: n.capacity, parallelism: n.parallelism, parentId: n.parentId, origin: "master" });
|
|
680
|
-
for (const m of readBoardEquipment(board)) this.equipment.set(m.id, { id: m.id, kind: m.kind, status: "idle", location: m.homeLocation, homeLocation: m.homeLocation, ...m.properties ? { properties: m.properties } : {}, ...m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}, origin: "master" });
|
|
681
|
-
for (const p of board.persons ?? []) this.persons.set(p.id, { id: p.id, personnelClassIds: p.personnelClassIds, status: "idle", ...p.homeLocation ? { location: p.homeLocation } : {}, ...p.properties ? { properties: p.properties } : {}, ...p.testSpecificationIds ? { testSpecificationIds: p.testSpecificationIds } : {} });
|
|
682
|
-
for (const
|
|
876
|
+
for (const m of readBoardEquipment(board)) this.equipment.set(m.id, { id: m.id, kind: m.kind, status: "idle", location: m.homeLocation, homeLocation: m.homeLocation, ...m.properties ? { properties: m.properties } : {}, ...m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}, ...effectiveOf(m), origin: "master" });
|
|
877
|
+
for (const p of board.persons ?? []) this.persons.set(p.id, { id: p.id, personnelClassIds: p.personnelClassIds, status: "idle", ...p.homeLocation ? { location: p.homeLocation } : {}, ...p.properties ? { properties: p.properties } : {}, ...p.testSpecificationIds ? { testSpecificationIds: p.testSpecificationIds } : {}, ...effectiveOf(p) });
|
|
878
|
+
for (const m of readBoardEquipment(board)) this.rememberShift(`eq:${m.id}`, m);
|
|
879
|
+
for (const p of board.persons ?? []) this.rememberShift(`person:${p.id}`, p);
|
|
880
|
+
for (const a of readBoardAssets(board)) this.assets.set(a.id, { id: a.id, assetClassIds: a.assetClassIds, location: a.homeLocation, status: "idle", ...a.properties ? { properties: a.properties } : {}, ...a.testSpecificationIds ? { testSpecificationIds: a.testSpecificationIds } : {}, ...effectiveOf(a) });
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* **구조를 갈아탄다** — 관측된 사실은 지키고 토폴로지만 새 선언으로 바꾼다.
|
|
884
|
+
*
|
|
885
|
+
* 공장은 바뀐다. 도장 부스를 넷 더 놓고, 라인을 하나 접는다. 그런데 지금까지는 구조가 바뀌면
|
|
886
|
+
* **그 트윈의 저널을 통째로 지우는 것**이 유일한 길이었다 — 안 지우면 옛 이벤트를 새 공장에 대고
|
|
887
|
+
* 접게 되어 이력이 거짓말을 한다. 역사를 잃거나 거짓말을 하거나, 둘뿐이었다.
|
|
888
|
+
*
|
|
889
|
+
* 셋째 길이 이것이다: 이벤트가 **자기 구조를 달고** 다니고, 재생은 구조가 바뀌는 지점에서 여기를
|
|
890
|
+
* 불러 갈아탄 뒤 이어 접는다. 그러면 "그때 그 공장의 사실" 로 계속 읽힌다.
|
|
891
|
+
*
|
|
892
|
+
* ── 무엇을 지키고 무엇을 버리는가 ───────────────────────────────────────
|
|
893
|
+
* **관측은 지킨다** — 물품·오더·작업·집합은 구조와 무관한 사실이다(팔레트는 부스를 늘려도 그대로다).
|
|
894
|
+
* **사라진 자원은 버린다** — 없어진 설비의 상태를 계속 들고 있으면 화면이 없는 설비를 그린다.
|
|
895
|
+
* 다만 **몇 개를 버렸는지 돌려준다.** 조용히 사라지면 사용자는 수가 줄어든 것을 눈치채지 못한다.
|
|
896
|
+
*
|
|
897
|
+
* 관측으로 알게 된 자리(`origin: 'observed'`)는 **새 마스터에 없어도 남긴다** — 마스터가 모르는
|
|
898
|
+
* 자리에서 물건이 실제로 보였다는 사실은 구조를 바꾼다고 사라지지 않는다.
|
|
899
|
+
*/
|
|
900
|
+
adoptStructure(board) {
|
|
901
|
+
this.utcOffsetMinutes = board.utcOffsetMinutes;
|
|
902
|
+
const locations = readBoardLocations(board);
|
|
903
|
+
const declaredLocationIds = new Set(locations.map((n) => n.id));
|
|
904
|
+
let locationsAdded = 0;
|
|
905
|
+
let locationsDropped = 0;
|
|
906
|
+
for (const [id, cur] of [...this.master]) {
|
|
907
|
+
if (declaredLocationIds.has(id) || cur.origin === "observed") continue;
|
|
908
|
+
this.master.delete(id);
|
|
909
|
+
locationsDropped++;
|
|
910
|
+
}
|
|
911
|
+
for (const n of locations) {
|
|
912
|
+
if (!this.master.has(n.id)) locationsAdded++;
|
|
913
|
+
this.master.set(n.id, { id: n.id, type: n.type, capacity: n.capacity, parallelism: n.parallelism, parentId: n.parentId, origin: "master" });
|
|
914
|
+
}
|
|
915
|
+
const equipment = readBoardEquipment(board);
|
|
916
|
+
const assets = readBoardAssets(board);
|
|
917
|
+
const persons = board.persons ?? [];
|
|
918
|
+
const dropMissing = (map, declared) => {
|
|
919
|
+
const ids = new Set(declared.map((d) => d.id));
|
|
920
|
+
let dropped = 0;
|
|
921
|
+
for (const id of [...map.keys()]) if (!ids.has(id)) {
|
|
922
|
+
map.delete(id);
|
|
923
|
+
dropped++;
|
|
924
|
+
}
|
|
925
|
+
return dropped;
|
|
926
|
+
};
|
|
927
|
+
const equipmentDropped = dropMissing(this.equipment, equipment);
|
|
928
|
+
const personsDropped = dropMissing(this.persons, persons);
|
|
929
|
+
const assetsDropped = dropMissing(this.assets, assets);
|
|
930
|
+
for (const m of equipment) {
|
|
931
|
+
if (!this.equipment.has(m.id))
|
|
932
|
+
this.equipment.set(m.id, { id: m.id, kind: m.kind, status: "idle", location: m.homeLocation, homeLocation: m.homeLocation, ...m.properties ? { properties: m.properties } : {}, ...m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}, ...effectiveOf(m), origin: "master" });
|
|
933
|
+
this.rememberShift(`eq:${m.id}`, m);
|
|
934
|
+
}
|
|
935
|
+
for (const p of persons) {
|
|
936
|
+
if (!this.persons.has(p.id))
|
|
937
|
+
this.persons.set(p.id, { id: p.id, personnelClassIds: p.personnelClassIds, status: "idle", ...p.homeLocation ? { location: p.homeLocation } : {}, ...p.properties ? { properties: p.properties } : {}, ...p.testSpecificationIds ? { testSpecificationIds: p.testSpecificationIds } : {}, ...effectiveOf(p) });
|
|
938
|
+
this.rememberShift(`person:${p.id}`, p);
|
|
939
|
+
}
|
|
940
|
+
for (const a of assets)
|
|
941
|
+
if (!this.assets.has(a.id))
|
|
942
|
+
this.assets.set(a.id, { id: a.id, assetClassIds: a.assetClassIds, location: a.homeLocation, status: "idle", ...a.properties ? { properties: a.properties } : {}, ...a.testSpecificationIds ? { testSpecificationIds: a.testSpecificationIds } : {}, ...effectiveOf(a) });
|
|
943
|
+
return { locationsAdded, locationsDropped, equipmentDropped, personsDropped, assetsDropped };
|
|
683
944
|
}
|
|
684
945
|
/** 마스터 동기 — 로케이션 추가/변경/제거. */
|
|
685
946
|
applyMaster(u) {
|
|
@@ -727,6 +988,7 @@ var ObservedReducer = class {
|
|
|
727
988
|
*/
|
|
728
989
|
stale(key, e) {
|
|
729
990
|
const at = Date.parse(String(e.eventTime ?? ""));
|
|
991
|
+
if (Number.isFinite(at) && (this.observedAtMs === void 0 || at > this.observedAtMs)) this.observedAtMs = at;
|
|
730
992
|
if (!Number.isFinite(at)) return false;
|
|
731
993
|
const recorded = Date.parse(String(e.data?.recordTime ?? ""));
|
|
732
994
|
const seen = this.lastAt.get(key);
|
|
@@ -741,6 +1003,14 @@ var ObservedReducer = class {
|
|
|
741
1003
|
}
|
|
742
1004
|
/** 대상별 마지막 반영 시각 — 순서 판정용(대상=EPC·작업·설비·오더 id). */
|
|
743
1005
|
lastAt = /* @__PURE__ */ new Map();
|
|
1006
|
+
/**
|
|
1007
|
+
* **미러의 "지금"** — 지금까지 들은 것 중 가장 늦은 발생 시각.
|
|
1008
|
+
*
|
|
1009
|
+
* 미러는 스스로 시간을 굴리지 않지만, 시각으로만 일어나는 사실(유효 기간 만료)을 판정하려면 기준이
|
|
1010
|
+
* 필요하다. 미러의 정직한 기준은 **"내가 마지막으로 들은 시점"** 이다. 아무것도 못 들었으면
|
|
1011
|
+
* 판정하지 않는다(모르면 단정하지 않는다).
|
|
1012
|
+
*/
|
|
1013
|
+
observedAtMs;
|
|
744
1014
|
/** 이벤트 1건 반영 — eventType 으로 EPCIS vs 운영 델타 분기. */
|
|
745
1015
|
apply(e) {
|
|
746
1016
|
this.revision++;
|
|
@@ -770,7 +1040,12 @@ var ObservedReducer = class {
|
|
|
770
1040
|
startedAtSimMs: d.startedAtSimMs,
|
|
771
1041
|
...d.personnel?.length ? { personnel: d.personnel.slice() } : {},
|
|
772
1042
|
...d.assets?.length ? { assets: d.assets.slice() } : {},
|
|
773
|
-
...d.resources?.length ? { resources: d.resources.slice() } : {}
|
|
1043
|
+
...d.resources?.length ? { resources: d.resources.slice() } : {},
|
|
1044
|
+
/* 실제 자재 이동 — 인원·설비와 같은 채널로 온다(실적을 한 곳에서 읽는다). */
|
|
1045
|
+
...d.materialActual?.length ? { materialActual: d.materialActual.map((r) => ({ ...r })) } : {},
|
|
1046
|
+
...d.priority !== void 0 ? { priority: d.priority } : {},
|
|
1047
|
+
...d.startTime ? { startTime: d.startTime } : {},
|
|
1048
|
+
...d.endTime ? { endTime: d.endTime } : {}
|
|
774
1049
|
});
|
|
775
1050
|
break;
|
|
776
1051
|
}
|
|
@@ -780,7 +1055,8 @@ var ObservedReducer = class {
|
|
|
780
1055
|
this.touchLocation(d.location);
|
|
781
1056
|
const known = this.equipment.get(d.moverId);
|
|
782
1057
|
const homeLocation = d.homeLocation ?? known?.homeLocation;
|
|
783
|
-
|
|
1058
|
+
const eqPeriod = d.effectiveStart || d.effectiveEnd ? effectiveOf(d) : effectiveOf(known);
|
|
1059
|
+
this.equipment.set(d.moverId, { id: d.moverId, kind: d.kind, status: d.status, location: d.location, ...homeLocation ? { homeLocation } : {}, taskId: d.taskId, motion: d.motion, origin: known?.origin ?? "observed", ...eqPeriod, ...d.held ? { held: true } : {} });
|
|
784
1060
|
break;
|
|
785
1061
|
}
|
|
786
1062
|
case OP_EVENT.person: {
|
|
@@ -795,7 +1071,11 @@ var ObservedReducer = class {
|
|
|
795
1071
|
...d.location ?? known?.location ? { location: d.location ?? known?.location } : {},
|
|
796
1072
|
...known?.properties ? { properties: known.properties } : {},
|
|
797
1073
|
...known?.testSpecificationIds ? { testSpecificationIds: known.testSpecificationIds } : {},
|
|
798
|
-
|
|
1074
|
+
/* 델타의 `offShift` 는 **선언이 없을 때만** 받는다 — 선언이 있으면 `snapshot()` 이 시각으로
|
|
1075
|
+
판정하고, 저장값을 함께 두면 교대 안으로 돌아왔을 때 옛 `true` 가 남는다(두 출처 = 갈라짐).
|
|
1076
|
+
남겨 두는 이유는 캘린더 없는 옛 저널 재생이다. */
|
|
1077
|
+
...d.offShift && !this.shifts.has(`person:${d.personId}`) ? { offShift: true } : {},
|
|
1078
|
+
...d.effectiveStart || d.effectiveEnd ? effectiveOf(d) : effectiveOf(known)
|
|
799
1079
|
});
|
|
800
1080
|
break;
|
|
801
1081
|
}
|
|
@@ -809,7 +1089,8 @@ var ObservedReducer = class {
|
|
|
809
1089
|
location: d.location ?? cur?.location,
|
|
810
1090
|
status: d.status,
|
|
811
1091
|
taskId: d.taskId,
|
|
812
|
-
...d.carrying ? { carrying: d.carrying } : {}
|
|
1092
|
+
...d.carrying ? { carrying: d.carrying } : {},
|
|
1093
|
+
...d.effectiveStart || d.effectiveEnd ? effectiveOf(d) : effectiveOf(cur)
|
|
813
1094
|
});
|
|
814
1095
|
this.touchLocation(d.location);
|
|
815
1096
|
break;
|
|
@@ -825,6 +1106,9 @@ var ObservedReducer = class {
|
|
|
825
1106
|
requested: d.requested,
|
|
826
1107
|
fulfilled: d.fulfilled,
|
|
827
1108
|
...d.lines?.length ? { lines: d.lines.map((l) => ({ ...l })) } : {},
|
|
1109
|
+
...d.priority !== void 0 ? { priority: d.priority } : {},
|
|
1110
|
+
...d.startTime ? { startTime: d.startTime } : {},
|
|
1111
|
+
...d.endTime ? { endTime: d.endTime } : {},
|
|
828
1112
|
held: d.held
|
|
829
1113
|
});
|
|
830
1114
|
break;
|
|
@@ -877,15 +1161,19 @@ var ObservedReducer = class {
|
|
|
877
1161
|
return;
|
|
878
1162
|
}
|
|
879
1163
|
const loc = ev.readPoint?.id;
|
|
880
|
-
const
|
|
1164
|
+
const all = ev.quantityList ?? [];
|
|
1165
|
+
const q = all[0];
|
|
881
1166
|
this.touchLocation(loc);
|
|
882
1167
|
for (const epc of ev.epcList) {
|
|
883
1168
|
if (envelope && this.stale(`item:${epc}`, envelope)) continue;
|
|
884
|
-
this.items.set(epc, this.mergeItem(epc, { location: loc, disposition: ev.disposition, ilmd: ev.ilmd }, q));
|
|
1169
|
+
this.items.set(epc, this.mergeItem(epc, { location: loc, disposition: ev.disposition, ilmd: ev.ilmd }, q, all));
|
|
885
1170
|
}
|
|
886
1171
|
if (!ev.epcList?.length) {
|
|
887
1172
|
for (const qe of ev.quantityList ?? []) {
|
|
888
|
-
if (qe?.epcClass)
|
|
1173
|
+
if (!qe?.epcClass) continue;
|
|
1174
|
+
const mine = all.filter((x) => x.epcClass === qe.epcClass);
|
|
1175
|
+
const subLotId = `${qe.epcClass}@${loc}`;
|
|
1176
|
+
this.items.set(subLotId, this.mergeItem(qe.epcClass, { location: loc, disposition: ev.disposition, ilmd: ev.ilmd }, qe, mine, subLotId));
|
|
889
1177
|
}
|
|
890
1178
|
}
|
|
891
1179
|
}
|
|
@@ -908,13 +1196,14 @@ var ObservedReducer = class {
|
|
|
908
1196
|
}
|
|
909
1197
|
return void 0;
|
|
910
1198
|
}
|
|
911
|
-
mergeItem(epc, patch, q) {
|
|
912
|
-
const cur = this.items.get(epc);
|
|
1199
|
+
mergeItem(epc, patch, q, all, subLotId) {
|
|
1200
|
+
const cur = this.items.get(subLotId ?? epc);
|
|
913
1201
|
const parsedClass = q?.epcClass ? parseEpc(q.epcClass) : void 0;
|
|
914
1202
|
const parsedSelf = parseEpc(epc);
|
|
915
1203
|
const classUri = q?.epcClass ?? (parsedSelf.instance ? void 0 : epc);
|
|
916
1204
|
return {
|
|
917
1205
|
epc,
|
|
1206
|
+
...subLotId ? { subLotId } : {},
|
|
918
1207
|
gtin: classUri ?? cur?.gtin,
|
|
919
1208
|
gtinKey: parsedClass?.gtinKey ?? parsedSelf.gtinKey ?? cur?.gtinKey,
|
|
920
1209
|
location: patch.location ?? cur?.location ?? "",
|
|
@@ -922,6 +1211,19 @@ var ObservedReducer = class {
|
|
|
922
1211
|
parent: cur?.parent ?? this.pendingParent.get(epc),
|
|
923
1212
|
qty: q?.quantity ?? cur?.qty,
|
|
924
1213
|
uom: q?.uom ?? cur?.uom,
|
|
1214
|
+
/*
|
|
1215
|
+
* 선언된 수량 전부 — 값이 없는 항목은 담지 않는다(모름을 0 으로 만들지 않는다).
|
|
1216
|
+
*
|
|
1217
|
+
* **한 항목뿐이면 담지 않는다.** 그 하나는 `qty`/`uom` 이 이미 무손실로 들고 있고, 1개짜리
|
|
1218
|
+
* 배열을 더 두면 같은 사실이 두 곳에 생긴다(그리고 시뮬은 그 배열을 만들지 않아 두 구동이
|
|
1219
|
+
* 갈라진다 — 적합성 하네스가 실제로 잡았다). `quantities` 의 뜻은 **추가 단위**다.
|
|
1220
|
+
* 읽을 때는 단위를 가리지 않는 `quantityIn` 을 쓴다(단일이든 복수든 같은 답).
|
|
1221
|
+
*/
|
|
1222
|
+
quantities: (() => {
|
|
1223
|
+
const declared = (all ?? []).filter((x) => typeof x.quantity === "number");
|
|
1224
|
+
if (declared.length < 2) return cur?.quantities;
|
|
1225
|
+
return declared.map((x) => ({ value: x.quantity, ...x.uom ? { uom: x.uom } : {} }));
|
|
1226
|
+
})(),
|
|
925
1227
|
/* 마스터데이터는 생겨날 때 한 번 정해진다 — 뒤 이벤트가 지우지 않게 기존 값을 남긴다. */
|
|
926
1228
|
ilmd: patch.ilmd ?? cur?.ilmd,
|
|
927
1229
|
expiry: this.expiryOf(patch.ilmd) ?? cur?.expiry,
|
|
@@ -939,6 +1241,47 @@ var ObservedReducer = class {
|
|
|
939
1241
|
}
|
|
940
1242
|
}
|
|
941
1243
|
/** 현재 투영 State — 자리 점유는 아이템 위치 집계로 유도(pure projection). */
|
|
1244
|
+
/**
|
|
1245
|
+
* 마지막으로 들은 발생 시각(ms) — **관측 구동의 "지금".**
|
|
1246
|
+
*
|
|
1247
|
+
* 시각으로만 바뀌는 사실(납기 초과·유효 기간·교대)을 판정하려면 기준이 필요하고, 관측 모드에는
|
|
1248
|
+
* 굴러가는 시계가 없다. 정직한 기준은 "내가 마지막으로 들은 시점" 이다. 아무것도 못 들었으면
|
|
1249
|
+
* `undefined` — 없는 기준으로 단정하지 않는다.
|
|
1250
|
+
*/
|
|
1251
|
+
get lastObservedMs() {
|
|
1252
|
+
return this.observedAtMs;
|
|
1253
|
+
}
|
|
1254
|
+
/** 선언된 교대만 기억한다 — 아무 선언이 없으면 키를 만들지 않는다(24시간 가용이 기존 거동). */
|
|
1255
|
+
rememberShift(key, r) {
|
|
1256
|
+
if (!r.workCalendar?.length && !r.window) return;
|
|
1257
|
+
this.shifts.set(key, {
|
|
1258
|
+
...r.window ? { window: r.window } : {},
|
|
1259
|
+
...r.workCalendar?.length ? { workCalendar: r.workCalendar } : {}
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* 지금 근무 시간 밖인가 — 시뮬과 **같은 함수**(`offCalendarAt`)를 미러의 시각으로 부른다.
|
|
1264
|
+
*
|
|
1265
|
+
* 판정을 델타로 받지 않는 이유는 유효 기간과 같다: 교대 경계는 **이벤트 없이 시각만으로** 넘어가므로,
|
|
1266
|
+
* 유휴 상태로 경계를 지난 자원은 실어 보낸 옛 판정에 머문다. 아무것도 못 들었으면 판정하지 않는다.
|
|
1267
|
+
*/
|
|
1268
|
+
offShiftPart(key) {
|
|
1269
|
+
const decl = this.shifts.get(key);
|
|
1270
|
+
if (!decl || this.observedAtMs === void 0) return {};
|
|
1271
|
+
if (offCalendarAt(decl, this.observedAtMs, this.utcOffsetMinutes)) {
|
|
1272
|
+
const why = offCalendarReasonAt(decl, this.observedAtMs, this.utcOffsetMinutes);
|
|
1273
|
+
return { offShift: true, ...why ? { offShiftReason: why } : {} };
|
|
1274
|
+
}
|
|
1275
|
+
const shift = activeShiftAt(decl.workCalendar, this.observedAtMs, this.utcOffsetMinutes);
|
|
1276
|
+
return shift ? { shift } : {};
|
|
1277
|
+
}
|
|
1278
|
+
/** 유효 기간 밖이면 그 이유를 붙인다 — 미러의 시각 기준은 `observedAtMs`(마지막으로 들은 시점). */
|
|
1279
|
+
effectivityPart(r) {
|
|
1280
|
+
if (!r.effectiveStart && !r.effectiveEnd) return {};
|
|
1281
|
+
if (this.observedAtMs === void 0) return {};
|
|
1282
|
+
const e = effectivityAt(r, new Date(this.observedAtMs).toISOString());
|
|
1283
|
+
return e ? { effectivity: e } : {};
|
|
1284
|
+
}
|
|
942
1285
|
snapshot() {
|
|
943
1286
|
const occ = /* @__PURE__ */ new Map();
|
|
944
1287
|
for (const it of this.items.values()) occ.set(it.location, (occ.get(it.location) ?? 0) + 1);
|
|
@@ -962,10 +1305,12 @@ var ObservedReducer = class {
|
|
|
962
1305
|
}),
|
|
963
1306
|
/* 들고 있는 것을 전부 내보낸다 — 축소하면 그 자리에서 정보가 사라진다. */
|
|
964
1307
|
items: [...this.items.values()].map((i) => ({ ...i })),
|
|
965
|
-
|
|
966
|
-
|
|
1308
|
+
/* 유효 기간 판정은 **저장하지 않고 여기서 낸다** — 시뮬과 **같은 함수**(`effectivityAt`)를 부른다.
|
|
1309
|
+
만료는 이벤트 없이 시각만으로 일어나므로, 델타로 받아 두면 유휴 자원이 영원히 유효하게 남는다. */
|
|
1310
|
+
persons: [...this.persons.values()].map((p) => ({ ...p, ...this.effectivityPart(p), ...this.offShiftPart(`person:${p.id}`) })),
|
|
1311
|
+
assets: [...this.assets.values()].map((a) => ({ ...a, ...this.effectivityPart(a) })),
|
|
967
1312
|
tasks: [...this.tasks.values()].map((t) => ({ ...t })),
|
|
968
|
-
equipment: [...this.equipment.values()].map((m) => ({ ...m })),
|
|
1313
|
+
equipment: [...this.equipment.values()].map((m) => ({ ...m, ...this.effectivityPart(m), ...this.offShiftPart(`eq:${m.id}`) })),
|
|
969
1314
|
orders: [...this.orders.values()].map((o) => ({ ...o }))
|
|
970
1315
|
};
|
|
971
1316
|
}
|
|
@@ -993,8 +1338,8 @@ var EventJournal = class {
|
|
|
993
1338
|
return this.events.slice(revision);
|
|
994
1339
|
}
|
|
995
1340
|
/** sim 시각(ISO eventTime) 이하 이벤트. */
|
|
996
|
-
untilSimTime(
|
|
997
|
-
return this.events.filter((e) => e.eventTime <=
|
|
1341
|
+
untilSimTime(iso2) {
|
|
1342
|
+
return this.events.filter((e) => e.eventTime <= iso2);
|
|
998
1343
|
}
|
|
999
1344
|
};
|
|
1000
1345
|
function replay(board, events) {
|
|
@@ -1002,6 +1347,16 @@ function replay(board, events) {
|
|
|
1002
1347
|
for (const e of events) proj.apply(e);
|
|
1003
1348
|
return proj.snapshot();
|
|
1004
1349
|
}
|
|
1350
|
+
function replaySegments(segments) {
|
|
1351
|
+
if (!segments.length) throw new Error("\uC7AC\uC0DD\uD560 \uB9C8\uB514\uAC00 \uC5C6\uB2E4 \u2014 \uAD6C\uC870\uB97C \uD558\uB098\uB3C4 \uC8FC\uC9C0 \uC54A\uC558\uB2E4");
|
|
1352
|
+
const proj = new ObservedReducer(segments[0].board);
|
|
1353
|
+
const shifts = [];
|
|
1354
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1355
|
+
if (i > 0) shifts.push({ index: i, ...proj.adoptStructure(segments[i].board) });
|
|
1356
|
+
for (const e of segments[i].events) proj.apply(e);
|
|
1357
|
+
}
|
|
1358
|
+
return { state: proj.snapshot(), shifts };
|
|
1359
|
+
}
|
|
1005
1360
|
|
|
1006
1361
|
// src/wms-profile.ts
|
|
1007
1362
|
var BIZSTEP = {
|
|
@@ -1227,6 +1582,46 @@ function foldTaskRecords(rows) {
|
|
|
1227
1582
|
return { records: [...byTask.values()], rowsRead, latestMs };
|
|
1228
1583
|
}
|
|
1229
1584
|
|
|
1585
|
+
// src/job-response.ts
|
|
1586
|
+
var iso = (ms2) => typeof ms2 === "number" ? new Date(ms2).toISOString() : void 0;
|
|
1587
|
+
var dataOf = (r) => {
|
|
1588
|
+
const p = r.payload;
|
|
1589
|
+
return r.data ?? p?.data ?? p ?? {};
|
|
1590
|
+
};
|
|
1591
|
+
function foldJobResponses(rows) {
|
|
1592
|
+
const { records } = foldTaskRecords(rows);
|
|
1593
|
+
const extra = /* @__PURE__ */ new Map();
|
|
1594
|
+
for (const r of rows) {
|
|
1595
|
+
const d = dataOf(r);
|
|
1596
|
+
const id = typeof d.taskId === "string" ? d.taskId : void 0;
|
|
1597
|
+
if (!id) continue;
|
|
1598
|
+
const cur = extra.get(id) ?? {};
|
|
1599
|
+
if (Array.isArray(d.personnel)) cur.personnel = d.personnel;
|
|
1600
|
+
if (Array.isArray(d.resources)) cur.resources = d.resources;
|
|
1601
|
+
else if (typeof d.resourceRef === "string" && !cur.resources) cur.resources = [d.resourceRef];
|
|
1602
|
+
if (Array.isArray(d.assets)) cur.assets = d.assets;
|
|
1603
|
+
if (Array.isArray(d.materialActual)) cur.material = d.materialActual;
|
|
1604
|
+
extra.set(id, cur);
|
|
1605
|
+
}
|
|
1606
|
+
return records.map((rec) => {
|
|
1607
|
+
const x = extra.get(rec.taskId) ?? {};
|
|
1608
|
+
const jobState = rec.completedMs !== void 0 ? "completed" : rec.startedMs !== void 0 ? "running" : "ready";
|
|
1609
|
+
const one = (ids) => ids?.length ? ids.map((id) => ({ id, quantity: 1 })) : void 0;
|
|
1610
|
+
return {
|
|
1611
|
+
id: rec.taskId,
|
|
1612
|
+
...rec.facets.order ? { jobOrderId: rec.facets.order } : {},
|
|
1613
|
+
...rec.facets.kind ? { workType: rec.facets.kind } : {},
|
|
1614
|
+
jobState,
|
|
1615
|
+
...iso(rec.startedMs) ? { startTime: iso(rec.startedMs) } : {},
|
|
1616
|
+
...iso(rec.completedMs) ? { endTime: iso(rec.completedMs) } : {},
|
|
1617
|
+
...one(x.personnel) ? { personnelActual: one(x.personnel) } : {},
|
|
1618
|
+
...one(x.resources) ? { equipmentActual: one(x.resources) } : {},
|
|
1619
|
+
...one(x.assets) ? { physicalAssetActual: one(x.assets) } : {},
|
|
1620
|
+
...x.material?.length ? { materialActual: x.material.map((m) => ({ ...m })) } : {}
|
|
1621
|
+
};
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1230
1625
|
// src/face2-adapter.ts
|
|
1231
1626
|
function get(obj, path) {
|
|
1232
1627
|
return path.split(".").reduce((o, k) => o == null ? o : o[k], obj);
|
|
@@ -1327,8 +1722,108 @@ var TwinRuntime = class {
|
|
|
1327
1722
|
}
|
|
1328
1723
|
};
|
|
1329
1724
|
|
|
1725
|
+
// src/capacity.ts
|
|
1726
|
+
function isoDurationHours(iso2) {
|
|
1727
|
+
if (!iso2) return 0;
|
|
1728
|
+
const m = /^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:([\d.]+)S)?)?$/.exec(iso2);
|
|
1729
|
+
if (!m) throw new Error(`\uAE30\uAC04 \uD45C\uAE30\uB97C \uC77D\uC744 \uC218 \uC5C6\uB2E4: ${iso2}`);
|
|
1730
|
+
return Number(m[1] ?? 0) * 24 + Number(m[2] ?? 0) + Number(m[3] ?? 0) / 60 + Number(m[4] ?? 0) / 3600;
|
|
1731
|
+
}
|
|
1732
|
+
function paramOf(op, id) {
|
|
1733
|
+
return op.parameters?.find((p) => p.id === id)?.value;
|
|
1734
|
+
}
|
|
1735
|
+
function workingTimeOfWeek(calendar, weekStartMs, utcOffsetMinutes) {
|
|
1736
|
+
if (!calendar?.length) return { hoursPerWeek: 7 * 24, daysPerWeek: 7 };
|
|
1737
|
+
let minutes = 0;
|
|
1738
|
+
const touched = /* @__PURE__ */ new Set();
|
|
1739
|
+
for (let m = 0; m < 7 * 24 * 60; m++) {
|
|
1740
|
+
if (!inWorkCalendarAt(calendar, weekStartMs + m * 6e4, utcOffsetMinutes)) continue;
|
|
1741
|
+
minutes++;
|
|
1742
|
+
touched.add(Math.floor((m + (utcOffsetMinutes ?? 0)) / (24 * 60)));
|
|
1743
|
+
}
|
|
1744
|
+
const declared = /* @__PURE__ */ new Set();
|
|
1745
|
+
for (const e of calendar) if (e.entryType !== "non-working") for (const d of e.daysOfWeek ?? []) declared.add(d);
|
|
1746
|
+
return { hoursPerWeek: minutes / 60, daysPerWeek: declared.size || touched.size };
|
|
1747
|
+
}
|
|
1748
|
+
function availabilityOf(records) {
|
|
1749
|
+
const ratios = records.map((r) => r.mtbfMs && r.mttrMs ? r.mtbfMs / (r.mtbfMs + r.mttrMs) : 1);
|
|
1750
|
+
return ratios.length ? ratios.reduce((a, b) => a + b, 0) / ratios.length : 1;
|
|
1751
|
+
}
|
|
1752
|
+
function analyzeCapacity(input) {
|
|
1753
|
+
const { hoursPerWeek, daysPerWeek } = workingTimeOfWeek(input.calendar, input.sampleWeekStartMs, input.utcOffsetMinutes);
|
|
1754
|
+
const demandPerHour = hoursPerWeek > 0 ? input.unitsPerDay * daysPerWeek / hoursPerWeek : 0;
|
|
1755
|
+
const equipmentByKind = /* @__PURE__ */ new Map();
|
|
1756
|
+
for (const e of input.equipment ?? []) {
|
|
1757
|
+
const list = equipmentByKind.get(e.kind) ?? [];
|
|
1758
|
+
list.push(e);
|
|
1759
|
+
equipmentByKind.set(e.kind, list);
|
|
1760
|
+
}
|
|
1761
|
+
const countByClass = (rows, field) => {
|
|
1762
|
+
const out = /* @__PURE__ */ new Map();
|
|
1763
|
+
for (const r of rows ?? []) for (const c of r[field] ?? []) out.set(c, (out.get(c) ?? 0) + 1);
|
|
1764
|
+
return out;
|
|
1765
|
+
};
|
|
1766
|
+
const personsByClass = countByClass(input.persons, "personnelClassIds");
|
|
1767
|
+
const assetsByClass = countByClass(input.assets, "assetClassIds");
|
|
1768
|
+
const slotsByLocationType = /* @__PURE__ */ new Map();
|
|
1769
|
+
for (const l of input.locations ?? [])
|
|
1770
|
+
if (l.type) slotsByLocationType.set(l.type, (slotsByLocationType.get(l.type) ?? 0) + Math.max(l.capacity ?? 1, 1));
|
|
1771
|
+
const order = input.route?.length ? input.route.map((k) => input.operations.find((o) => o.key === k)).filter((o) => !!o) : [...input.operations];
|
|
1772
|
+
let downstream = 1;
|
|
1773
|
+
const reversed = [];
|
|
1774
|
+
for (const op of [...order].reverse()) {
|
|
1775
|
+
const cycleHours = isoDurationHours(op.duration) + isoDurationHours(paramOf(op, "setupDuration"));
|
|
1776
|
+
downstream *= Number(paramOf(op, "yield") ?? 1);
|
|
1777
|
+
const requiredPerHour = downstream > 0 ? demandPerHour / downstream : Infinity;
|
|
1778
|
+
const requirements = [];
|
|
1779
|
+
const add = (axis, className, quantity, available, availability) => {
|
|
1780
|
+
const q = Math.max(quantity, 1);
|
|
1781
|
+
requirements.push({
|
|
1782
|
+
axis,
|
|
1783
|
+
className,
|
|
1784
|
+
quantity: q,
|
|
1785
|
+
available,
|
|
1786
|
+
availability,
|
|
1787
|
+
perHour: cycleHours > 0 ? Math.floor(available / q) / cycleHours * availability : Infinity
|
|
1788
|
+
});
|
|
1789
|
+
};
|
|
1790
|
+
const equipSpecs = op.equipmentSpecification?.length ? op.equipmentSpecification.map((s) => ({ className: s.equipmentClass ?? op.resourceType ?? "", quantity: s.quantity })) : op.resourceType ? [{ className: op.resourceType, quantity: 1 }] : [];
|
|
1791
|
+
for (const s of equipSpecs) {
|
|
1792
|
+
if (!s.className) continue;
|
|
1793
|
+
const records = equipmentByKind.get(s.className) ?? [];
|
|
1794
|
+
add("equipment", s.className, s.quantity, records.length, availabilityOf(records));
|
|
1795
|
+
}
|
|
1796
|
+
for (const s of op.personnelSpecification ?? [])
|
|
1797
|
+
if (s.personnelClass) add("personnel", s.personnelClass, s.quantity, personsByClass.get(s.personnelClass) ?? 0, 1);
|
|
1798
|
+
for (const s of op.physicalAssetSpecification ?? [])
|
|
1799
|
+
if (s.assetClass) add("asset", s.assetClass, s.quantity, assetsByClass.get(s.assetClass) ?? 0, 1);
|
|
1800
|
+
if (op.locationType) add("location", op.locationType, 1, slotsByLocationType.get(op.locationType) ?? 0, 1);
|
|
1801
|
+
const constraint = requirements.length ? requirements.reduce((a, b) => a.perHour <= b.perHour ? a : b) : void 0;
|
|
1802
|
+
const perHour = constraint ? constraint.perHour : Infinity;
|
|
1803
|
+
reversed.push({ operation: op.key, cycleHours, perHour, requiredPerHour, ok: perHour >= requiredPerHour, constraint, requirements });
|
|
1804
|
+
}
|
|
1805
|
+
const operations = reversed.reverse();
|
|
1806
|
+
const tightest = operations.length ? operations.reduce((a, b) => a.perHour / a.requiredPerHour <= b.perHour / b.requiredPerHour ? a : b) : void 0;
|
|
1807
|
+
const headroom = operations.length ? Math.min(...operations.map((o) => o.perHour / o.requiredPerHour)) : Infinity;
|
|
1808
|
+
return {
|
|
1809
|
+
workingHoursPerWeek: hoursPerWeek,
|
|
1810
|
+
workingDaysPerWeek: daysPerWeek,
|
|
1811
|
+
demandPerHour,
|
|
1812
|
+
operations,
|
|
1813
|
+
...tightest ? { bottleneck: { operation: tightest.operation, axis: tightest.constraint?.axis, className: tightest.constraint?.className } } : {},
|
|
1814
|
+
maxUnitsPerDay: input.unitsPerDay * headroom,
|
|
1815
|
+
ok: operations.every((o) => o.ok)
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1330
1819
|
// src/flow-engine.ts
|
|
1331
1820
|
var BASE_EPOCH = Date.parse("2026-01-01T00:00:00Z");
|
|
1821
|
+
function effectiveOnly(r) {
|
|
1822
|
+
return {
|
|
1823
|
+
...r.effectiveStart ? { effectiveStart: r.effectiveStart } : {},
|
|
1824
|
+
...r.effectiveEnd ? { effectiveEnd: r.effectiveEnd } : {}
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1332
1827
|
function mulberry32(seed) {
|
|
1333
1828
|
let a = seed >>> 0;
|
|
1334
1829
|
const fn = () => {
|
|
@@ -1342,7 +1837,7 @@ function mulberry32(seed) {
|
|
|
1342
1837
|
} });
|
|
1343
1838
|
return fn;
|
|
1344
1839
|
}
|
|
1345
|
-
function deriveAttentions(view, acked) {
|
|
1840
|
+
function deriveAttentions(view, acked, nowIso) {
|
|
1346
1841
|
const out = [];
|
|
1347
1842
|
for (const m of view.equipment) {
|
|
1348
1843
|
if (m.status === "down") {
|
|
@@ -1377,6 +1872,30 @@ function deriveAttentions(view, acked) {
|
|
|
1377
1872
|
}
|
|
1378
1873
|
}
|
|
1379
1874
|
}
|
|
1875
|
+
const QUEUE_FLOOR = 5;
|
|
1876
|
+
const shutdown = (view.equipment?.length ?? 0) > 0 && view.equipment.every((m) => m.offShift);
|
|
1877
|
+
if (view.tasks?.length && !shutdown) {
|
|
1878
|
+
const waiting = /* @__PURE__ */ new Map();
|
|
1879
|
+
const working = /* @__PURE__ */ new Map();
|
|
1880
|
+
for (const t of view.tasks) {
|
|
1881
|
+
const bucket = t.status === "created" || t.status === "assigned" ? waiting : t.status === "in-progress" ? working : void 0;
|
|
1882
|
+
if (bucket) bucket.set(t.kind, (bucket.get(t.kind) ?? 0) + 1);
|
|
1883
|
+
}
|
|
1884
|
+
for (const [operation, queued] of waiting) {
|
|
1885
|
+
if (queued < QUEUE_FLOOR) continue;
|
|
1886
|
+
const active = working.get(operation) ?? 0;
|
|
1887
|
+
const ratio = queued / Math.max(active, 1);
|
|
1888
|
+
if (active > 0 && ratio < 3) continue;
|
|
1889
|
+
out.push({
|
|
1890
|
+
id: `work-backlog:${operation}`,
|
|
1891
|
+
kind: "work-backlog",
|
|
1892
|
+
severity: active === 0 || ratio >= 6 ? "high" : "medium",
|
|
1893
|
+
anchor: { operation },
|
|
1894
|
+
params: { operation, queued, active, ratio: Math.round(ratio * 10) / 10 },
|
|
1895
|
+
recommendedActions: [{ code: "advice.add-resource" }, { code: "advice.downstream-priority" }]
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1380
1899
|
for (const m of view.equipment) {
|
|
1381
1900
|
const total = (m.goodCount ?? 0) + (m.scrapCount ?? 0);
|
|
1382
1901
|
if (total >= 10) {
|
|
@@ -1406,6 +1925,21 @@ function deriveAttentions(view, acked) {
|
|
|
1406
1925
|
suggestedAction: { code: "act.resume-order", command: CMD.orderResume, args: { orderId: o.id } }
|
|
1407
1926
|
});
|
|
1408
1927
|
}
|
|
1928
|
+
for (const o of view.orders) {
|
|
1929
|
+
if (o.status === "completed" || o.status === "fulfilled") continue;
|
|
1930
|
+
if (dueStatusOf(o, nowIso) !== "late") continue;
|
|
1931
|
+
const overdueMs = Date.parse(nowIso) - Date.parse(o.endTime);
|
|
1932
|
+
out.push({
|
|
1933
|
+
id: `late:${o.id}`,
|
|
1934
|
+
kind: "order-late",
|
|
1935
|
+
severity: overdueMs >= 36e5 ? "critical" : "high",
|
|
1936
|
+
anchor: { orderId: o.id },
|
|
1937
|
+
params: { orderId: o.id, endTime: o.endTime, overdueMs, ...o.priority !== void 0 ? { priority: o.priority } : {} },
|
|
1938
|
+
/* 조치는 권고만 둔다 — 지연을 커널이 스스로 해소할 수단이 없다(자원을 늘리거나 우선순위를
|
|
1939
|
+
올리는 것은 사람의 결정이다). 없는 조치를 만들어 붙이지 않는다. */
|
|
1940
|
+
recommendedActions: [{ code: "advice.raise-priority" }, { code: "advice.add-resource" }]
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1409
1943
|
if (acked) {
|
|
1410
1944
|
for (const a of out) if (acked.has(a.id)) a.state = "acknowledged";
|
|
1411
1945
|
}
|
|
@@ -1438,6 +1972,11 @@ var FlowEngine = class {
|
|
|
1438
1972
|
equipment = /* @__PURE__ */ new Map();
|
|
1439
1973
|
/** 등급 정의(표준 `<X>Class`) — 상속·유효기간 판정의 재료. 선언 안 하면 비어 있고, 소속 그대로 판정한다. */
|
|
1440
1974
|
classDefs = {};
|
|
1975
|
+
/**
|
|
1976
|
+
* 품목 정의 색인(표준 `MaterialDefinition`) — 단위 환산의 **유일한 근거**.
|
|
1977
|
+
* 비어 있으면 환산하지 않는다(계수를 모르는데 값을 만들지 않는다).
|
|
1978
|
+
*/
|
|
1979
|
+
materialDefs = /* @__PURE__ */ new Map();
|
|
1441
1980
|
/** 사람 — 선언하지 않으면 빈 맵(인원 제약 없는 트윈, 기존 거동). */
|
|
1442
1981
|
persons = /* @__PURE__ */ new Map();
|
|
1443
1982
|
/** 물리 자산 — 선언하지 않으면 빈 맵(자산 제약 없는 트윈, 기존 거동). */
|
|
@@ -1485,11 +2024,12 @@ var FlowEngine = class {
|
|
|
1485
2024
|
loadBoard(def) {
|
|
1486
2025
|
this.boardDef = def;
|
|
1487
2026
|
for (const n of readBoardLocations(def)) this.locations.set(n.id, { id: n.id, type: n.type, capacity: n.capacity, parallelism: n.parallelism, occupancy: 0, status: "idle", parentId: n.parentId });
|
|
1488
|
-
this.classDefs = { personnel: def.personnelClasses, equipment: def.equipmentClasses, asset: def.assetClasses };
|
|
1489
|
-
|
|
1490
|
-
for (const
|
|
2027
|
+
this.classDefs = { personnel: def.personnelClasses, equipment: def.equipmentClasses, asset: def.assetClasses, material: def.materialClasses };
|
|
2028
|
+
this.materialDefs = new Map((def.materialDefinitions ?? []).filter((d) => d?.id).map((d) => [d.id, d]));
|
|
2029
|
+
for (const p of def.persons ?? []) this.persons.set(p.id, { id: p.id, personnelClassIds: p.personnelClassIds, status: "idle", taskId: null, window: p.window, ...p.workCalendar ? { workCalendar: p.workCalendar } : {}, ...p.homeLocation ? { location: p.homeLocation } : {}, ...p.properties ? { properties: p.properties } : {}, ...p.testSpecificationIds ? { testSpecificationIds: p.testSpecificationIds } : {}, ...effectiveOnly(p) });
|
|
2030
|
+
for (const a of readBoardAssets(def)) this.assets.set(a.id, { id: a.id, assetClassIds: a.assetClassIds, location: a.homeLocation, status: "idle", taskId: null, ...a.properties ? { properties: a.properties } : {}, ...a.testSpecificationIds ? { testSpecificationIds: a.testSpecificationIds } : {}, ...effectiveOnly(a) });
|
|
1491
2031
|
for (const m of readBoardEquipment(def)) {
|
|
1492
|
-
const eq = { id: m.id, kind: m.kind, location: m.homeLocation, homeLocation: m.homeLocation, ...m.properties ? { properties: m.properties } : {}, ...m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}, status: "idle", taskId: null, runMs: 0, setupMs: 0, downMs: 0, goodCount: 0, scrapCount: 0, window: m.window };
|
|
2032
|
+
const eq = { id: m.id, kind: m.kind, location: m.homeLocation, homeLocation: m.homeLocation, ...m.properties ? { properties: m.properties } : {}, ...m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}, status: "idle", taskId: null, runMs: 0, setupMs: 0, downMs: 0, goodCount: 0, scrapCount: 0, window: m.window, ...m.workCalendar ? { workCalendar: m.workCalendar } : {}, ...effectiveOnly(m) };
|
|
1493
2033
|
if (m.mtbfMs !== void 0) {
|
|
1494
2034
|
eq.mtbfMs = m.mtbfMs;
|
|
1495
2035
|
eq.mttrMs = m.mttrMs;
|
|
@@ -1519,20 +2059,38 @@ var FlowEngine = class {
|
|
|
1519
2059
|
* 오더는 남은 데맨드(라인별 requested-fulfilled)를 'created' 로 복원(현재 재고에서 재계획).
|
|
1520
2060
|
* 진행 중 개별 task 의 내부 상태는 관측만으론 복원 불가 → 재계획에 맡김(정직한 한계).
|
|
1521
2061
|
*/
|
|
2062
|
+
/**
|
|
2063
|
+
* **이어서 센다** — 재기동 뒤 저널을 이어 쓸 때.
|
|
2064
|
+
*
|
|
2065
|
+
* 커널은 리비전을 0부터 센다. 새로 시작하는 우주라면 맞지만, **이어지는 현실**(sim-world)에서는
|
|
2066
|
+
* 그 번호가 이미 저널에 있는 행과 겹친다 — 같은 트윈에 revision 7이 두 개 생기고, 재생 순서가
|
|
2067
|
+
* 뒤섞이며, 시간여행이 엉뚱한 시점을 답한다. 조용히 깨지는 종류다(오류가 안 난다).
|
|
2068
|
+
*
|
|
2069
|
+
* 그래서 이어 쓰기 전에 마지막 번호를 알려 준다. **뒤로는 못 간다** — 뒤로 가는 것은 곧 겹치는
|
|
2070
|
+
* 것이고, 그것을 허용하면 이 함수가 막으려던 일이 이 함수를 통해 일어난다.
|
|
2071
|
+
*/
|
|
2072
|
+
resumeRevision(from) {
|
|
2073
|
+
if (!Number.isFinite(from) || from < 0) throw new Error(`\uC774\uC5B4 \uC140 \uB9AC\uBE44\uC804\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uB2E4: ${from}`);
|
|
2074
|
+
if (from < this.revision) throw new Error(`\uB9AC\uBE44\uC804\uC744 \uB4A4\uB85C \uB418\uB3CC\uB9B4 \uC218 \uC5C6\uB2E4: ${this.revision} \u2192 ${from} (\uACB9\uCE58\uB294 \uBC88\uD638\uAC00 \uC0DD\uAE34\uB2E4)`);
|
|
2075
|
+
this.revision = from;
|
|
2076
|
+
}
|
|
1522
2077
|
hydrateObserved(snap, orders = []) {
|
|
1523
2078
|
for (const n of snap.locations) {
|
|
1524
2079
|
this.locations.set(n.id, { id: n.id, type: n.type, capacity: n.capacity ?? 0, parallelism: n.parallelism, occupancy: n.occupancy ?? 0, status: n.status ?? "idle", parentId: n.parentId });
|
|
1525
2080
|
}
|
|
1526
2081
|
this.items.clear();
|
|
1527
2082
|
for (const it of snap.items) {
|
|
1528
|
-
this.items.set(it
|
|
2083
|
+
this.items.set(itemKeyOf(it), {
|
|
1529
2084
|
/* 품번 키·로트는 식별자에서 파생되므로 심지 않는다(스냅샷이 다시 낸다 — 두 벌을 두면 어긋난다). */
|
|
1530
2085
|
epc: it.epc,
|
|
1531
2086
|
location: it.location,
|
|
1532
2087
|
disposition: it.disposition ?? DISP.sellable,
|
|
2088
|
+
...it.subLotId ? { subLotId: it.subLotId } : {},
|
|
2089
|
+
...it.definitionId ? { definitionId: it.definitionId } : {},
|
|
1533
2090
|
gtin: it.gtin,
|
|
1534
2091
|
qty: it.qty ?? 1,
|
|
1535
2092
|
uom: it.uom,
|
|
2093
|
+
...it.quantities?.length ? { quantities: it.quantities } : {},
|
|
1536
2094
|
parent: it.parent,
|
|
1537
2095
|
carriedBy: it.carriedBy,
|
|
1538
2096
|
expiry: it.expiry,
|
|
@@ -1541,7 +2099,9 @@ var FlowEngine = class {
|
|
|
1541
2099
|
}
|
|
1542
2100
|
for (const m of snap.equipment) {
|
|
1543
2101
|
const oee = m.oee;
|
|
2102
|
+
const prev = this.equipment.get(m.id);
|
|
1544
2103
|
this.equipment.set(m.id, {
|
|
2104
|
+
...prev ?? {},
|
|
1545
2105
|
id: m.id,
|
|
1546
2106
|
kind: m.kind,
|
|
1547
2107
|
location: m.location ?? "",
|
|
@@ -1552,22 +2112,102 @@ var FlowEngine = class {
|
|
|
1552
2112
|
setupMs: oee?.setupMs ?? 0,
|
|
1553
2113
|
downMs: oee?.downMs ?? 0,
|
|
1554
2114
|
goodCount: oee?.goodCount ?? 0,
|
|
1555
|
-
scrapCount: oee?.scrapCount ?? 0
|
|
2115
|
+
scrapCount: oee?.scrapCount ?? 0,
|
|
2116
|
+
/* 계획 정지를 이어받는다 — 잃으면 씨앗이 **정비 중인 설비를 가용으로 놓고** 미래를 굴린다
|
|
2117
|
+
(예측이 낙관 쪽으로 치우친다). 씨앗 왕복 대조가 이것을 잡았다. */
|
|
2118
|
+
...m.held ? { held: true } : {},
|
|
2119
|
+
/* 관측 스냅샷이 들고 있는 것은 관측을 따른다(미러가 보드에서 읽어 실어 온다). */
|
|
2120
|
+
...m.properties ? { properties: m.properties } : {},
|
|
2121
|
+
...m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {},
|
|
2122
|
+
...effectiveOnly(m)
|
|
1556
2123
|
});
|
|
1557
2124
|
}
|
|
1558
2125
|
for (const p of snap.persons ?? []) {
|
|
1559
|
-
this.persons.
|
|
2126
|
+
const prev = this.persons.get(p.id);
|
|
2127
|
+
this.persons.set(p.id, {
|
|
2128
|
+
...prev ?? {},
|
|
2129
|
+
id: p.id,
|
|
2130
|
+
personnelClassIds: p.personnelClassIds ?? prev?.personnelClassIds,
|
|
2131
|
+
status: "idle",
|
|
2132
|
+
taskId: null,
|
|
2133
|
+
...p.location ? { location: p.location } : {},
|
|
2134
|
+
...p.properties ? { properties: p.properties } : {},
|
|
2135
|
+
...p.testSpecificationIds ? { testSpecificationIds: p.testSpecificationIds } : {},
|
|
2136
|
+
...effectiveOnly(p)
|
|
2137
|
+
});
|
|
1560
2138
|
}
|
|
1561
2139
|
for (const a of snap.assets ?? []) {
|
|
1562
|
-
this.assets.
|
|
2140
|
+
const prev = this.assets.get(a.id);
|
|
2141
|
+
this.assets.set(a.id, {
|
|
2142
|
+
...prev ?? {},
|
|
2143
|
+
id: a.id,
|
|
2144
|
+
assetClassIds: a.assetClassIds ?? prev?.assetClassIds,
|
|
2145
|
+
location: a.location,
|
|
2146
|
+
status: "idle",
|
|
2147
|
+
taskId: null,
|
|
2148
|
+
carrying: a.carrying,
|
|
2149
|
+
...a.properties ? { properties: a.properties } : {},
|
|
2150
|
+
...a.testSpecificationIds ? { testSpecificationIds: a.testSpecificationIds } : {},
|
|
2151
|
+
...effectiveOnly(a)
|
|
2152
|
+
});
|
|
1563
2153
|
}
|
|
2154
|
+
const observedOrders = orders.length ? orders : (snap.orders ?? []).filter((o) => o.requested !== void 0).map((o) => ({
|
|
2155
|
+
orderId: o.id,
|
|
2156
|
+
kind: o.kind,
|
|
2157
|
+
status: o.status,
|
|
2158
|
+
requested: o.requested,
|
|
2159
|
+
fulfilled: o.fulfilled ?? 0,
|
|
2160
|
+
held: o.held,
|
|
2161
|
+
lines: o.lines,
|
|
2162
|
+
/* 약속한 시각과 우선순위를 함께 옮긴다 — 없으면 관측 커널이 **"늦었나" 를 판단할 재료가
|
|
2163
|
+
없다.** 라이브 트윈이 납기 초과를 한 번도 보고하지 못한 원인 중 하나였다. */
|
|
2164
|
+
...o.endTime ? { endTime: o.endTime } : {},
|
|
2165
|
+
...o.startTime ? { startTime: o.startTime } : {},
|
|
2166
|
+
...o.priority !== void 0 ? { priority: o.priority } : {}
|
|
2167
|
+
}));
|
|
2168
|
+
for (const o of observedOrders) {
|
|
2169
|
+
const lines = (o.lines ?? []).map((l) => ({ gtin: l.gtin, requested: l.requested - (l.fulfilled ?? 0) })).filter((l) => l.requested > 0);
|
|
2170
|
+
const remaining = lines.length ? lines.reduce((s, l) => s + l.requested, 0) : Math.max(0, (o.requested ?? 0) - (o.fulfilled ?? 0));
|
|
2171
|
+
if (remaining <= 0) continue;
|
|
2172
|
+
this.orders.set(o.orderId, {
|
|
2173
|
+
id: o.orderId,
|
|
2174
|
+
kind: o.kind,
|
|
2175
|
+
status: "created",
|
|
2176
|
+
requested: remaining,
|
|
2177
|
+
fulfilled: 0,
|
|
2178
|
+
bizTransaction: "",
|
|
2179
|
+
allocated: [],
|
|
2180
|
+
picked: [],
|
|
2181
|
+
shipmentEpc: null,
|
|
2182
|
+
lines,
|
|
2183
|
+
/* **보류를 이어받는다** — 잃으면 씨앗이 사람이 일부러 멈춘 오더를 다시 계획해 내보낸다.
|
|
2184
|
+
씨앗 왕복 대조가 이것을 잡았다(그 전에는 사람이 코드를 읽어야만 알 수 있었다). */
|
|
2185
|
+
...o.held ? { held: true } : {},
|
|
2186
|
+
...o.endTime ? { endTime: o.endTime } : {},
|
|
2187
|
+
...o.startTime ? { startTime: o.startTime } : {},
|
|
2188
|
+
...o.priority !== void 0 ? { priority: o.priority } : {}
|
|
2189
|
+
});
|
|
2190
|
+
}
|
|
2191
|
+
const seededOrderIds = new Set(this.orders.keys());
|
|
2192
|
+
let orphaned = 0;
|
|
1564
2193
|
for (const t of snap.tasks ?? []) {
|
|
1565
2194
|
if (t.status === "completed") continue;
|
|
2195
|
+
const ref = t.itemRefs?.[0];
|
|
2196
|
+
if (ref && !this.itemByRef(ref)) {
|
|
2197
|
+
orphaned++;
|
|
2198
|
+
continue;
|
|
2199
|
+
}
|
|
2200
|
+
if (t.orderId && !seededOrderIds.has(t.orderId)) {
|
|
2201
|
+
orphaned++;
|
|
2202
|
+
continue;
|
|
2203
|
+
}
|
|
1566
2204
|
const known = typeof t.remainingMs === "number" && Number.isFinite(t.remainingMs);
|
|
1567
2205
|
this.tasks.set(t.id, {
|
|
1568
2206
|
id: t.id,
|
|
1569
2207
|
kind: t.kind,
|
|
1570
2208
|
status: known && t.status === "in-progress" ? "in-progress" : "created",
|
|
2209
|
+
/* 이미 일어난 자재 이동은 **씨앗에도 남는다** — 잃으면 실적이 재기동마다 지워진다. */
|
|
2210
|
+
...t.materialActual?.length ? { materialActual: t.materialActual.map((r) => ({ ...r })) } : {},
|
|
1571
2211
|
itemEpc: t.itemRefs?.[0] ?? "",
|
|
1572
2212
|
fromNode: t.fromNode ?? "",
|
|
1573
2213
|
toNode: t.toNode ?? "",
|
|
@@ -1597,12 +2237,10 @@ var FlowEngine = class {
|
|
|
1597
2237
|
}
|
|
1598
2238
|
}
|
|
1599
2239
|
}
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
if (remaining <= 0) continue;
|
|
1605
|
-
this.orders.set(o.orderId, { id: o.orderId, kind: o.kind, status: "created", requested: remaining, fulfilled: 0, bizTransaction: "", allocated: [], picked: [], shipmentEpc: null, lines });
|
|
2240
|
+
if (orphaned) {
|
|
2241
|
+
console.warn(
|
|
2242
|
+
`[twin-kernel] seed skipped ${orphaned} in-flight task(s) whose item is no longer in state \u2014 they cannot be continued (the item was consumed/shipped, or the observation did not carry it).`
|
|
2243
|
+
);
|
|
1606
2244
|
}
|
|
1607
2245
|
}
|
|
1608
2246
|
/** what-if 구성 변주 — 자리 용량 변경(fork 대상). 존재하면 true. */
|
|
@@ -1633,6 +2271,15 @@ var FlowEngine = class {
|
|
|
1633
2271
|
* State 델타를 유발한다(command → 행위 → 관측 폐루프).
|
|
1634
2272
|
*/
|
|
1635
2273
|
_acked = /* @__PURE__ */ new Set();
|
|
2274
|
+
/**
|
|
2275
|
+
* 완료 시점에 **주체가 사라져 접은 작업 수** — 조용한 누락이 되지 않게 센다.
|
|
2276
|
+
*
|
|
2277
|
+
* 물품이 포장·출하·소비로 없어지는 것은 정상이지만, 그 물품을 향한 작업이 남아 있었다는 것은
|
|
2278
|
+
* 상류에 어긋남이 있다는 신호다. 0 이 아니면 그 사실을 소비처가 볼 수 있어야 한다.
|
|
2279
|
+
*/
|
|
2280
|
+
abandonedTasks = 0;
|
|
2281
|
+
/** 주목 신호가 **처음 성립한 시각**(id → ISO). 조건이 사라지면 지운다 — 재발은 새 시작이다. */
|
|
2282
|
+
_attentionSince = /* @__PURE__ */ new Map();
|
|
1636
2283
|
// 확인(ack)된 주목 신호 id — 조건 지속돼도 acknowledged 로 표시(재발 시 재활성)
|
|
1637
2284
|
dispatch(cmd) {
|
|
1638
2285
|
const ok = () => ({ commandId: cmd.commandId, accepted: true });
|
|
@@ -1754,6 +2401,8 @@ var FlowEngine = class {
|
|
|
1754
2401
|
return {
|
|
1755
2402
|
revision: this.revision,
|
|
1756
2403
|
simClockMs: this.clockMs,
|
|
2404
|
+
nowTime: this.now(),
|
|
2405
|
+
// 트윈의 "지금" — 관측 모드면 마지막으로 들은 시각(§nowMs)
|
|
1757
2406
|
/* 출처 표시 — 보드(마스터)에서 온 자리다. 미러는 관측으로 알게 된 자리를 'observed' 로 구별하는데,
|
|
1758
2407
|
* 시뮬이 아무 표시도 안 하면 소비처가 두 스냅샷을 같은 규칙으로 읽지 못한다. */
|
|
1759
2408
|
locations: [...this.locations.values()].map((n) => {
|
|
@@ -1763,7 +2412,7 @@ var FlowEngine = class {
|
|
|
1763
2412
|
}),
|
|
1764
2413
|
items: [...this.items.values()].map((i) => this.itemState(i)),
|
|
1765
2414
|
equipment: [...this.equipment.values()].map((m) => {
|
|
1766
|
-
const s = { id: m.id, kind: m.kind, location: m.location, ...m.homeLocation ? { homeLocation: m.homeLocation } : {}, status: m.status, taskId: m.taskId ?? void 0, oee: this.oeeOf(m), held: m.held, ...m.properties ? { properties: m.properties } : {}, ...m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}, origin: "master", ...this.offShift(m) ? { offShift: true } : {} };
|
|
2415
|
+
const s = { id: m.id, kind: m.kind, location: m.location, ...m.homeLocation ? { homeLocation: m.homeLocation } : {}, status: m.status, taskId: m.taskId ?? void 0, oee: this.oeeOf(m), held: m.held, ...m.properties ? { properties: m.properties } : {}, ...m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}, origin: "master", ...this.offShift(m) ? { offShift: true, ...this.offReason(m) } : {}, ...this.effectivePart(m), ...this.shiftOf(m) ? { shift: this.shiftOf(m) } : {} };
|
|
1767
2416
|
const t = m.taskId ? this.tasks.get(m.taskId) : void 0;
|
|
1768
2417
|
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 };
|
|
1769
2418
|
return s;
|
|
@@ -1773,6 +2422,7 @@ var FlowEngine = class {
|
|
|
1773
2422
|
if (a.carrying) st.carrying = a.carrying;
|
|
1774
2423
|
if (a.properties) st.properties = a.properties;
|
|
1775
2424
|
if (a.testSpecificationIds) st.testSpecificationIds = a.testSpecificationIds;
|
|
2425
|
+
Object.assign(st, this.effectivePart(a));
|
|
1776
2426
|
return st;
|
|
1777
2427
|
}),
|
|
1778
2428
|
persons: [...this.persons.values()].map((p) => {
|
|
@@ -1780,7 +2430,13 @@ var FlowEngine = class {
|
|
|
1780
2430
|
if (p.location) st.location = p.location;
|
|
1781
2431
|
if (p.properties) st.properties = p.properties;
|
|
1782
2432
|
if (p.testSpecificationIds) st.testSpecificationIds = p.testSpecificationIds;
|
|
1783
|
-
if (this.personOffShift(p))
|
|
2433
|
+
if (this.personOffShift(p)) {
|
|
2434
|
+
st.offShift = true;
|
|
2435
|
+
Object.assign(st, this.offReason(p));
|
|
2436
|
+
}
|
|
2437
|
+
const sh = this.shiftOf(p);
|
|
2438
|
+
if (sh) st.shift = sh;
|
|
2439
|
+
Object.assign(st, this.effectivePart(p));
|
|
1784
2440
|
return st;
|
|
1785
2441
|
}),
|
|
1786
2442
|
/* 스냅샷이 **델타보다 가난하면 안 된다** — 예전에는 소요·남은 시간을 빼고 내보내서, 이 스냅샷으로
|
|
@@ -1800,7 +2456,11 @@ var FlowEngine = class {
|
|
|
1800
2456
|
...t.status === "in-progress" ? { remainingMs: t.remainingMs, startedAtSimMs: t.startedAtSimMs, progress: this.progressOf(t) } : {},
|
|
1801
2457
|
...t.personnel?.length ? { personnel: t.personnel.slice() } : {},
|
|
1802
2458
|
...t.assets?.length ? { assets: t.assets.slice() } : {},
|
|
1803
|
-
...t.resources?.length ? { resources: t.resources.slice() } : {}
|
|
2459
|
+
...t.resources?.length ? { resources: t.resources.slice() } : {},
|
|
2460
|
+
...t.materialActual?.length ? { materialActual: t.materialActual.map((r) => ({ ...r })) } : {},
|
|
2461
|
+
...t.priority !== void 0 ? { priority: t.priority } : {},
|
|
2462
|
+
...t.startTime ? { startTime: t.startTime } : {},
|
|
2463
|
+
...t.endTime ? { endTime: t.endTime } : {}
|
|
1804
2464
|
})),
|
|
1805
2465
|
orders: [...this.orders.values()].map((o) => ({
|
|
1806
2466
|
id: o.id,
|
|
@@ -1810,6 +2470,9 @@ var FlowEngine = class {
|
|
|
1810
2470
|
requested: o.requested,
|
|
1811
2471
|
fulfilled: o.fulfilled,
|
|
1812
2472
|
...o.lines?.length ? { lines: o.lines.map((l) => ({ gtin: l.gtin, requested: l.requested })) } : {},
|
|
2473
|
+
...o.priority !== void 0 ? { priority: o.priority } : {},
|
|
2474
|
+
...o.startTime ? { startTime: o.startTime } : {},
|
|
2475
|
+
...o.endTime ? { endTime: o.endTime } : {},
|
|
1813
2476
|
held: o.held
|
|
1814
2477
|
})),
|
|
1815
2478
|
attentions: this.computeAttentions()
|
|
@@ -1821,12 +2484,26 @@ var FlowEngine = class {
|
|
|
1821
2484
|
* 도메인별 추가 판단은 서브클래스가 override 로 확장(super.computeAttentions() 합성).
|
|
1822
2485
|
*/
|
|
1823
2486
|
computeAttentions() {
|
|
2487
|
+
const now = this.now();
|
|
1824
2488
|
const out = deriveAttentions(
|
|
1825
|
-
{
|
|
1826
|
-
|
|
2489
|
+
{
|
|
2490
|
+
equipment: [...this.equipment.values()],
|
|
2491
|
+
locations: [...this.locations.values()],
|
|
2492
|
+
orders: [...this.orders.values()],
|
|
2493
|
+
tasks: [...this.tasks.values()]
|
|
2494
|
+
},
|
|
2495
|
+
this._acked,
|
|
2496
|
+
now
|
|
2497
|
+
// 지연 판정의 "지금" — 관측 중이면 마지막으로 들은 시각이다(§nowMs)
|
|
1827
2498
|
);
|
|
1828
2499
|
const present = new Set(out.map((a) => a.id));
|
|
2500
|
+
for (const a of out) {
|
|
2501
|
+
const first = this._attentionSince.get(a.id) ?? now;
|
|
2502
|
+
this._attentionSince.set(a.id, first);
|
|
2503
|
+
a.timeStamp = first;
|
|
2504
|
+
}
|
|
1829
2505
|
for (const id of [...this._acked]) if (!present.has(id)) this._acked.delete(id);
|
|
2506
|
+
for (const id of [...this._attentionSince.keys()]) if (!present.has(id)) this._attentionSince.delete(id);
|
|
1830
2507
|
return out;
|
|
1831
2508
|
}
|
|
1832
2509
|
/**
|
|
@@ -1853,8 +2530,42 @@ var FlowEngine = class {
|
|
|
1853
2530
|
return clone;
|
|
1854
2531
|
}
|
|
1855
2532
|
// ── 보호 헬퍼 (도메인 hook 에서 사용) ──────────────────────────────────────
|
|
2533
|
+
/**
|
|
2534
|
+
* 이 커널의 **지금**(ms) — 시각으로 바뀌는 모든 판정의 단일 기준.
|
|
2535
|
+
*
|
|
2536
|
+
* ── 관측 모드에서 시계가 멈춰 있었다 ────────────────────────────────────
|
|
2537
|
+
* `apply()` 는 `clockMs` 를 밀지 않는다(관측은 시간을 굴리지 않는다). 그래서 라이브 트윈의 "지금" 이
|
|
2538
|
+
* **BASE_EPOCH(2026-01-01)에 얼어 있었다.** 실 이벤트는 실제 시각을 달고 오므로 결과가 이렇게 된다:
|
|
2539
|
+
* · **납기 초과를 영원히 보고하지 않는다** — 실 납기가 항상 미래로 보인다
|
|
2540
|
+
* · 폐기·도입예정 판정이 틀린다(§EffectivePeriod)
|
|
2541
|
+
* · 교대 판정이 틀린다(§offCalendarAt)
|
|
2542
|
+
* 셋 다 "시각으로만 바뀌는 사실" 이라 같은 결함이었다. 그래서 기준을 **한 곳**으로 모은다.
|
|
2543
|
+
*
|
|
2544
|
+
* 관측 중이면 **마지막으로 들은 발생 시각**을 쓴다(미러의 `lastObservedMs` 와 같은 기준 — 규칙 한 벌).
|
|
2545
|
+
* 아직 아무것도 못 들었으면 시뮬 기준으로 떨어진다(그때는 판정할 사실도 없다).
|
|
2546
|
+
*/
|
|
2547
|
+
nowMs() {
|
|
2548
|
+
const observed = this.observeMode ? this.observer?.lastObservedMs : void 0;
|
|
2549
|
+
return observed ?? BASE_EPOCH + this.clockMs;
|
|
2550
|
+
}
|
|
1856
2551
|
now() {
|
|
1857
|
-
return new Date(
|
|
2552
|
+
return new Date(this.nowMs()).toISOString();
|
|
2553
|
+
}
|
|
2554
|
+
/**
|
|
2555
|
+
* 자극이 선언한 **약속**을 오더 필드로 — 표준 `OperationsRequest.Priority`·`StartTime`·`EndTime`.
|
|
2556
|
+
*
|
|
2557
|
+
* **base 에 한 곳만 둔다.** 오더를 만드는 곳이 도메인마다 있어(WMS 판매오더·MES 작업지시·YMS
|
|
2558
|
+
* 어포인트먼트) 각자 계산하면 시간축이나 규약이 갈린다 — 실제로 한 번 갈렸다(납기를 `clockMs` 로
|
|
2559
|
+
* 재고 `now()` 와 비교해 갓 만든 오더가 56년 늦은 것으로 판정됐다).
|
|
2560
|
+
*
|
|
2561
|
+
* 선언이 없으면 **빈 객체**를 준다 — 없는 약속을 만들지 않는다.
|
|
2562
|
+
*/
|
|
2563
|
+
promiseOf(spec) {
|
|
2564
|
+
const at = this.now();
|
|
2565
|
+
return {
|
|
2566
|
+
...spec.priority !== void 0 ? { priority: spec.priority } : {},
|
|
2567
|
+
...spec.promisedLeadMinutes !== void 0 ? { startTime: at, endTime: new Date(Date.parse(at) + spec.promisedLeadMinutes * 6e4).toISOString() } : {}
|
|
2568
|
+
};
|
|
1858
2569
|
}
|
|
1859
2570
|
randInt(min, max) {
|
|
1860
2571
|
return max <= min ? min : min + Math.floor(this.rng() * (max - min + 1));
|
|
@@ -1906,6 +2617,41 @@ var FlowEngine = class {
|
|
|
1906
2617
|
loadOperations(ops = []) {
|
|
1907
2618
|
for (const o of ops) if (o?.key) this.operationSpecs.set(o.key, o);
|
|
1908
2619
|
}
|
|
2620
|
+
/**
|
|
2621
|
+
* 라우트(공정 순서) — 수율을 거슬러 올릴 때 필요하다. 기본은 모른다(선언 순서를 쓴다).
|
|
2622
|
+
* 생산 정의를 가진 커널이 override 해서 자기 라우트를 답한다.
|
|
2623
|
+
*/
|
|
2624
|
+
routeKeys() {
|
|
2625
|
+
return void 0;
|
|
2626
|
+
}
|
|
2627
|
+
/**
|
|
2628
|
+
* **이 공장이 하루 몇 대를 낼 수 있는가** — 굴려 보지 않고 답한다.
|
|
2629
|
+
*
|
|
2630
|
+
* 커널이 직접 답하는 이유: 필요한 사실이 전부 여기 있다(공정 명세·설비와 신뢰도·인원·물리자산·
|
|
2631
|
+
* 자리·근무 달력·시각 기준). 밖에서 모으면 그 값을 옮겨 적게 되고, 한쪽만 바뀌는 순간 "충분하다"
|
|
2632
|
+
* 가 조용히 거짓이 된다. 계산 자체는 순수 함수(`analyzeCapacity`)에 맡긴다.
|
|
2633
|
+
*
|
|
2634
|
+
* 달력은 **자원이 선언한 것**을 쓴다. 자원마다 다른 달력을 쓰는 현장이면 대표를 고를 수 없으므로
|
|
2635
|
+
* 그 사실을 결과에 실어 보낸다(`mixedCalendars`) — 조용히 하나를 골라 계산하면 천장이 틀린 채로
|
|
2636
|
+
* 그럴듯해 보인다.
|
|
2637
|
+
*/
|
|
2638
|
+
capacity(opts) {
|
|
2639
|
+
const calendars = /* @__PURE__ */ new Map();
|
|
2640
|
+
for (const m of this.equipment.values()) if (m.workCalendar?.length) calendars.set(JSON.stringify(m.workCalendar), m.workCalendar);
|
|
2641
|
+
const analysis = analyzeCapacity({
|
|
2642
|
+
operations: [...this.operationSpecs.values()],
|
|
2643
|
+
...this.routeKeys() ? { route: this.routeKeys() } : {},
|
|
2644
|
+
equipment: [...this.equipment.values()].map((m) => ({ kind: m.kind, mtbfMs: m.mtbfMs, mttrMs: m.mttrMs })),
|
|
2645
|
+
persons: [...this.persons.values()],
|
|
2646
|
+
assets: [...this.assets.values()],
|
|
2647
|
+
locations: [...this.locations.values()],
|
|
2648
|
+
...calendars.size === 1 ? { calendar: [...calendars.values()][0] } : {},
|
|
2649
|
+
...this.boardDef?.utcOffsetMinutes !== void 0 ? { utcOffsetMinutes: this.boardDef.utcOffsetMinutes } : {},
|
|
2650
|
+
sampleWeekStartMs: opts.sampleWeekStartMs,
|
|
2651
|
+
unitsPerDay: opts.unitsPerDay
|
|
2652
|
+
});
|
|
2653
|
+
return { ...analysis, mixedCalendars: calendars.size > 1 };
|
|
2654
|
+
}
|
|
1909
2655
|
/**
|
|
1910
2656
|
* task 소요 산출 — 우선순위: **① 추정기(이력 보정) → ② 명세(ISA-95 Duration + 변동) → ③ 도메인 상수.**
|
|
1911
2657
|
*
|
|
@@ -2015,8 +2761,18 @@ var FlowEngine = class {
|
|
|
2015
2761
|
defaultDurations: operations.filter((o) => o.duration === "default").length
|
|
2016
2762
|
};
|
|
2017
2763
|
}
|
|
2018
|
-
/**
|
|
2764
|
+
/**
|
|
2765
|
+
* `skuMix` 에서 weight 로 gtin 선택(rng) — 도착·오더 자극의 품목 결정.
|
|
2766
|
+
*
|
|
2767
|
+
* **빈 목록이면 고르지 않는다**(`undefined`). 예전에는 `mix[mix.length - 1].gtin` 으로 떨어져
|
|
2768
|
+
* `mix[-1]` 이 undefined 가 되고 거기서 던졌다 — 그 예외가 서버까지 올라가 **정확도 추세 전체를
|
|
2769
|
+
* 죽였다**(품목 구성을 선언하지 않은 자극 하나가 예측 전체를 껐다).
|
|
2770
|
+
*
|
|
2771
|
+
* 없는 품목을 지어내지 않는다: 무엇을 만들지 모르면 **만들지 않는 것**이 맞고, 부르는 쪽이
|
|
2772
|
+
* 그 사실을 알고 건너뛴다.
|
|
2773
|
+
*/
|
|
2019
2774
|
pickGtin(mix) {
|
|
2775
|
+
if (!mix?.length) return void 0;
|
|
2020
2776
|
const total = mix.reduce((s, m) => s + m.weight, 0);
|
|
2021
2777
|
let r = this.rng() * total;
|
|
2022
2778
|
for (const m of mix) {
|
|
@@ -2206,21 +2962,26 @@ var FlowEngine = class {
|
|
|
2206
2962
|
...t.personnel?.length ? { personnel: t.personnel.slice() } : {},
|
|
2207
2963
|
...t.assets?.length ? { assets: t.assets.slice() } : {},
|
|
2208
2964
|
...t.resources?.length ? { resources: t.resources.slice() } : {},
|
|
2965
|
+
/* 실제 자재 이동 — 인원·설비와 같은 채널(실적을 한 곳에서 읽는다). */
|
|
2966
|
+
...t.materialActual?.length ? { materialActual: t.materialActual.map((r) => ({ ...r })) } : {},
|
|
2209
2967
|
...t.durationMs ? { durationMs: t.durationMs } : {},
|
|
2968
|
+
...t.priority !== void 0 ? { priority: t.priority } : {},
|
|
2969
|
+
...t.startTime ? { startTime: t.startTime } : {},
|
|
2970
|
+
...t.endTime ? { endTime: t.endTime } : {},
|
|
2210
2971
|
...inProgress ? { remainingMs: t.remainingMs, startedAtSimMs: t.startedAtSimMs, progress: t.durationMs ? done / t.durationMs : void 0 } : {}
|
|
2211
2972
|
});
|
|
2212
2973
|
}
|
|
2213
2974
|
/** 사람 상태 전이 — 설비와 별개 채널(어휘가 다르다: 고장이 아니라 교대·투입). */
|
|
2214
2975
|
emitAsset(a) {
|
|
2215
|
-
this.emitOp(OP_EVENT.asset, { assetId: a.id, assetClassIds: a.assetClassIds, status: a.status, location: a.location, taskId: a.taskId ?? void 0, carrying: a.carrying });
|
|
2976
|
+
this.emitOp(OP_EVENT.asset, { assetId: a.id, assetClassIds: a.assetClassIds, status: a.status, location: a.location, taskId: a.taskId ?? void 0, carrying: a.carrying, ...effectiveOnly(a) });
|
|
2216
2977
|
}
|
|
2217
2978
|
emitPerson(p) {
|
|
2218
|
-
this.emitOp(OP_EVENT.person, { personId: p.id, personnelClassIds: p.personnelClassIds, status: p.status, taskId: p.taskId ?? void 0, ...p.location ? { location: p.location } : {}, ...
|
|
2979
|
+
this.emitOp(OP_EVENT.person, { personId: p.id, personnelClassIds: p.personnelClassIds, status: p.status, taskId: p.taskId ?? void 0, ...p.location ? { location: p.location } : {}, ...effectiveOnly(p) });
|
|
2219
2980
|
}
|
|
2220
2981
|
/** 설비 상태 전이 — `taskId` 를 함께 싣는다(사람·자산 델타와 같은 규칙). 없으면 미러가 "이 설비가
|
|
2221
2982
|
* 무슨 일을 하는 중인가" 를 알 수 없어 작업↔자원 연결이 한쪽에서만 성립한다. */
|
|
2222
2983
|
emitEquipment(m, motion) {
|
|
2223
|
-
this.emitOp(OP_EVENT.equipment, { moverId: m.id, kind: m.kind, status: m.status, location: m.location, ...m.homeLocation ? { homeLocation: m.homeLocation } : {}, taskId: m.taskId ?? void 0, motion });
|
|
2984
|
+
this.emitOp(OP_EVENT.equipment, { moverId: m.id, kind: m.kind, status: m.status, location: m.location, ...m.homeLocation ? { homeLocation: m.homeLocation } : {}, taskId: m.taskId ?? void 0, motion, ...m.held ? { held: true } : {}, ...effectiveOnly(m) });
|
|
2224
2985
|
}
|
|
2225
2986
|
/** 오더 델타 — **라인까지 싣는다.** 라인이 빠지면 미러가 남은 데맨드를 라인별로 재계획할 수 없다. */
|
|
2226
2987
|
emitOrder(o) {
|
|
@@ -2231,6 +2992,9 @@ var FlowEngine = class {
|
|
|
2231
2992
|
requested: o.requested,
|
|
2232
2993
|
fulfilled: o.fulfilled,
|
|
2233
2994
|
held: o.held,
|
|
2995
|
+
...o.priority !== void 0 ? { priority: o.priority } : {},
|
|
2996
|
+
...o.startTime ? { startTime: o.startTime } : {},
|
|
2997
|
+
...o.endTime ? { endTime: o.endTime } : {},
|
|
2234
2998
|
...o.lines?.length ? { lines: o.lines.map((l) => ({ gtin: l.gtin, requested: l.requested })) } : {}
|
|
2235
2999
|
});
|
|
2236
3000
|
}
|
|
@@ -2301,7 +3065,8 @@ var FlowEngine = class {
|
|
|
2301
3065
|
if (!want) continue;
|
|
2302
3066
|
const avail = [...this.assets.values()].filter(
|
|
2303
3067
|
/* 인원과 같은 규칙 — 상속을 타고 닫아 판정하고 유효기간 밖은 제외한다. 요구는 등급 하나+수량. */
|
|
2304
|
-
(a) => a.status === "idle" && !picked.includes(a.id) && (
|
|
3068
|
+
(a) => a.status === "idle" && !picked.includes(a.id) && !this.outOfEffect(a) && // 폐기한 팔레트는 풀에서 빠진다
|
|
3069
|
+
(req.assetClass === void 0 || classClosure(a.assetClassIds, this.classDefs.asset, this.now()).has(req.assetClass))
|
|
2305
3070
|
);
|
|
2306
3071
|
if (avail.length < want) return null;
|
|
2307
3072
|
for (let i = 0; i < want; i++) picked.push(avail[i].id);
|
|
@@ -2319,7 +3084,7 @@ var FlowEngine = class {
|
|
|
2319
3084
|
a.taskId = t.id;
|
|
2320
3085
|
if (t.itemEpc) {
|
|
2321
3086
|
a.carrying = t.itemEpc;
|
|
2322
|
-
const it = this.
|
|
3087
|
+
const it = this.itemByRef(t.itemEpc);
|
|
2323
3088
|
if (it) it.carriedBy = a.id;
|
|
2324
3089
|
}
|
|
2325
3090
|
this.emitAsset(a);
|
|
@@ -2350,6 +3115,194 @@ var FlowEngine = class {
|
|
|
2350
3115
|
* 여기서는 고르기만 하고 잡지 않는다: 설비까지 확보된 뒤 `assignCrew` 가 확정한다
|
|
2351
3116
|
* (반쯤 잡고 실패하면 사람이 아무 일도 못 하면서 묶인다).
|
|
2352
3117
|
*/
|
|
3118
|
+
/**
|
|
3119
|
+
* 필요 자재를 확보한다 — **ISA-95 `OperationsSegment.MaterialSpecification`**(`use: 'consumed'`).
|
|
3120
|
+
*
|
|
3121
|
+
* ── 4대 자원 중 자재만 게이트가 없었다 ────────────────────────────────────
|
|
3122
|
+
* 인원·자산·설비는 모자라면 기다리는데 **자재는 없어도 작업이 시작됐다.** 그래서 부품이 떨어져
|
|
3123
|
+
* 라인이 서는 상황이 예측에 아예 나타나지 않았다 — 트레일러 조립 공장에서 그것은 가장 흔한 정지다.
|
|
3124
|
+
*
|
|
3125
|
+
* 인원·자산과 **같은 규칙**이다: 등급(또는 품목)으로 요구하고, **부분 확보 없이 전량 아니면 대기.**
|
|
3126
|
+
* 자재는 **작업이 일어나는 자리에 있어야** 한다(다른 창고의 부품은 지금 쓸 수 없다).
|
|
3127
|
+
*
|
|
3128
|
+
* 소비 시점은 **작업 시작**이다 — 자재는 들어가는 순간 없어진다(완료 시점에 빼면 그 사이 다른
|
|
3129
|
+
* 작업이 같은 부품을 또 잡는다). 산출(`produced`)은 여기서 다루지 않는다(완료 시점의 일이다).
|
|
3130
|
+
*/
|
|
3131
|
+
claimMaterials(t) {
|
|
3132
|
+
const need = (this.operationSpecs.get(t.kind)?.materialSpecification ?? []).filter((m) => m.use === "consumed");
|
|
3133
|
+
if (!need.length) return [];
|
|
3134
|
+
const at = t.toNode;
|
|
3135
|
+
const picked = [];
|
|
3136
|
+
const takenSoFar = /* @__PURE__ */ new Map();
|
|
3137
|
+
for (const req of need) {
|
|
3138
|
+
let remaining = Math.max(0, req.quantity ?? 0);
|
|
3139
|
+
if (!remaining) continue;
|
|
3140
|
+
for (const it of this.items.values()) {
|
|
3141
|
+
if (remaining <= 0) break;
|
|
3142
|
+
if (it.location !== at) continue;
|
|
3143
|
+
if (!this.materialMatches(it, req)) continue;
|
|
3144
|
+
const already = takenSoFar.get(it.epc) ?? 0;
|
|
3145
|
+
const avail = Math.max(0, (it.qty ?? 1) - already);
|
|
3146
|
+
if (avail <= 0) continue;
|
|
3147
|
+
const take = Math.min(avail, remaining);
|
|
3148
|
+
picked.push({ epc: it.epc, take });
|
|
3149
|
+
takenSoFar.set(it.epc, already + take);
|
|
3150
|
+
remaining -= take;
|
|
3151
|
+
}
|
|
3152
|
+
if (remaining > 0) return null;
|
|
3153
|
+
}
|
|
3154
|
+
return picked;
|
|
3155
|
+
}
|
|
3156
|
+
/** 이 물품이 명세를 만족하나 — 품목 지목이 우선, 없으면 등급(상속을 타고 닫는다). */
|
|
3157
|
+
materialMatches(it, req) {
|
|
3158
|
+
const defId = it.definitionId ?? it.gtin;
|
|
3159
|
+
if (req.materialDefinition) return defId === req.materialDefinition;
|
|
3160
|
+
if (req.materialClass) {
|
|
3161
|
+
const def = defId ? this.materialDefs.get(defId) : void 0;
|
|
3162
|
+
return classClosure(def?.materialClassIds, this.classDefs.material, this.now()).has(req.materialClass);
|
|
3163
|
+
}
|
|
3164
|
+
return true;
|
|
3165
|
+
}
|
|
3166
|
+
/**
|
|
3167
|
+
* 산출 자재를 만든다 — ISA-95 `MaterialSpecification`(`use: 'produced'`).
|
|
3168
|
+
*
|
|
3169
|
+
* **소비의 짝이다**: 자재는 작업이 시작될 때 들어가고 끝날 때 나온다. 산출이 없으면 조립 공정이
|
|
3170
|
+
* 부품만 먹고 아무것도 내놓지 않아, 다음 공정이 영원히 재료를 기다린다(라인 전체가 한 칸에서 멈춘다).
|
|
3171
|
+
*
|
|
3172
|
+
* 산출물의 자리는 **작업이 끝난 자리**(`toNode`)다. 식별자는 품목 식별자에 작업 id 를 붙여 만든다 —
|
|
3173
|
+
* **EPC 처럼 보이게 꾸미지 않는다**(상류가 준 직렬번호와 구별돼야 한다). 상류가 진짜 식별자를
|
|
3174
|
+
* 말해 주면 그것이 들어올 자리는 관측 경로다.
|
|
3175
|
+
*
|
|
3176
|
+
* 같은 품목이 그 자리에 이미 있으면 **수량을 더한다** — 새 줄을 만들면 같은 자리의 같은 로트가
|
|
3177
|
+
* 둘로 갈려 재고가 부푼다(§MaterialSubLot 에서 겪은 것과 반대 방향의 같은 오류).
|
|
3178
|
+
*/
|
|
3179
|
+
produceMaterials(t) {
|
|
3180
|
+
const made = (this.operationSpecs.get(t.kind)?.materialSpecification ?? []).filter((m) => m.use === "produced");
|
|
3181
|
+
if (!made.length) return;
|
|
3182
|
+
for (const spec of made) {
|
|
3183
|
+
const qty = Math.max(0, spec.quantity ?? 0);
|
|
3184
|
+
if (!qty) continue;
|
|
3185
|
+
const gtin = spec.materialDefinition;
|
|
3186
|
+
if (!gtin) continue;
|
|
3187
|
+
const at = t.toNode;
|
|
3188
|
+
const key = subLotIdOf(gtin, at);
|
|
3189
|
+
const existing = this.items.get(key);
|
|
3190
|
+
const total = (existing?.qty ?? 0) + qty;
|
|
3191
|
+
if (existing) existing.qty = total;
|
|
3192
|
+
else {
|
|
3193
|
+
this.items.set(key, { epc: gtin, subLotId: key, gtin, qty: total, location: at, disposition: DISP.in_progress, ...spec.uom ? { uom: spec.uom } : {} });
|
|
3194
|
+
const n = this.locations.get(at);
|
|
3195
|
+
if (n) n.occupancy++;
|
|
3196
|
+
}
|
|
3197
|
+
this.recordMaterialActual(t, gtin, "produced", qty, spec.uom);
|
|
3198
|
+
this.emit(objectEvent({
|
|
3199
|
+
eventTime: this.now(),
|
|
3200
|
+
action: "ADD",
|
|
3201
|
+
bizStep: CBV_BIZSTEP.commissioning,
|
|
3202
|
+
disposition: DISP.in_progress,
|
|
3203
|
+
epcList: [],
|
|
3204
|
+
quantityList: [{ epcClass: gtin, quantity: total, ...spec.uom ? { uom: spec.uom } : {} }],
|
|
3205
|
+
readPoint: at,
|
|
3206
|
+
bizLocation: at
|
|
3207
|
+
}));
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
/**
|
|
3211
|
+
* 이 작업이 **딛고 선 것**이 아직 있나 — 없으면 무엇이 없는지 답한다.
|
|
3212
|
+
*
|
|
3213
|
+
* 작업은 혼자 서지 못한다: 옮길 **물품**과, (있다면) 그것을 시킨 **오더** 위에 선다. 진행 중에
|
|
3214
|
+
* 둘 중 하나가 사라질 수 있다 — 물품은 포장·출하·소비로, 오더는 이미 이행돼 씨앗이 심지 않아서.
|
|
3215
|
+
*
|
|
3216
|
+
* 그때 도메인 훅은 없는 것을 딛으려다 던진다(`order.gtin` · `item.location`). 그 예외 하나가
|
|
3217
|
+
* **예측 전체를 죽였다** — 사용자에게는 기능이 통째로 사라진 것으로 보였다. 그래서 완료 **전에**
|
|
3218
|
+
* 여기서 묻고, 없으면 그 작업만 접는다.
|
|
3219
|
+
*/
|
|
3220
|
+
missingContextOf(t) {
|
|
3221
|
+
if (t.itemEpc && !this.itemByRef(t.itemEpc)) return "item";
|
|
3222
|
+
if (t.orderId && !this.orders.has(t.orderId)) return "order";
|
|
3223
|
+
if (!this.canComplete(t)) return "domain";
|
|
3224
|
+
return void 0;
|
|
3225
|
+
}
|
|
3226
|
+
/**
|
|
3227
|
+
* 이 도메인이 이 작업을 **끝맺을 수 있나** — 코어가 모르는 조건은 도메인이 답한다.
|
|
3228
|
+
*
|
|
3229
|
+
* 코어는 물품과 오더까지만 안다. 그런데 도메인은 더 필요할 수 있다 — MES 는 완료 시점에 **오더와
|
|
3230
|
+
* 제품 정의**를 딛고 서고, 그 중 하나만 없어도 던진다. 코어가 그 조건을 추측하면 도메인마다 다른
|
|
3231
|
+
* 가정을 코어에 박게 되므로(방언), **묻는다.**
|
|
3232
|
+
*
|
|
3233
|
+
* 기본은 `true` — 대부분의 작업은 코어가 확인한 것으로 충분하다.
|
|
3234
|
+
*/
|
|
3235
|
+
canComplete(_t) {
|
|
3236
|
+
return true;
|
|
3237
|
+
}
|
|
3238
|
+
/**
|
|
3239
|
+
* 작업이 가리키는 물품을 찾는다 — **참조와 키가 다를 수 있다.**
|
|
3240
|
+
*
|
|
3241
|
+
* 물품 맵의 키는 `itemKeyOf`(직렬 물품은 `epc`, 로트의 부분은 `subLotId`)인데, 작업은 로트 식별자
|
|
3242
|
+
* (`itemEpc`)로 가리킨다. 비직렬 로트에서는 둘이 **다르다** — 그래서 그냥 `get` 하면 못 찾는다.
|
|
3243
|
+
* 실제로 그 회귀를 냈다: 예측(씨앗) 경로에서 `item.location = …` 이 `undefined` 위에서 던져
|
|
3244
|
+
* **정확도 추세 전체가 서버 오류로 죽었다.**
|
|
3245
|
+
*
|
|
3246
|
+
* **부분이 여럿이면 풀지 않는다** — 어느 부분을 가리키는지 알 수 없고, 아무거나 고르면 그 뒤
|
|
3247
|
+
* 이동·처분이 엉뚱한 자리에 적힌다. 그때는 참조가 부족한 것이고, 부르는 쪽이 그 사실을 말해야 한다.
|
|
3248
|
+
*/
|
|
3249
|
+
itemByRef(ref) {
|
|
3250
|
+
const exact = this.items.get(ref);
|
|
3251
|
+
if (exact) return exact;
|
|
3252
|
+
let hit;
|
|
3253
|
+
for (const it of this.items.values()) {
|
|
3254
|
+
if (it.epc !== ref) continue;
|
|
3255
|
+
if (hit) return void 0;
|
|
3256
|
+
hit = it;
|
|
3257
|
+
}
|
|
3258
|
+
return hit;
|
|
3259
|
+
}
|
|
3260
|
+
/**
|
|
3261
|
+
* 실제 자재 이동을 작업에 적어 둔다 — ISA-95 `JobResponse.MaterialActual`.
|
|
3262
|
+
* 같은 품목·같은 쓰임은 **한 줄로 합친다**(줄을 늘리면 실적을 세는 쪽이 중복을 걷어내야 한다).
|
|
3263
|
+
*/
|
|
3264
|
+
recordMaterialActual(t, definitionId, use, quantity, uom) {
|
|
3265
|
+
if (!definitionId || !(quantity > 0)) return;
|
|
3266
|
+
const rows = t.materialActual ??= [];
|
|
3267
|
+
const hit = rows.find((r) => r.definitionId === definitionId && r.use === use && r.uom === uom);
|
|
3268
|
+
if (hit) hit.quantity += quantity;
|
|
3269
|
+
else rows.push({ definitionId, use, quantity, ...uom ? { uom } : {} });
|
|
3270
|
+
}
|
|
3271
|
+
/** 확보한 자재를 **작업 시작 시점에 소비**한다 — 수량이 0 이 되면 물품 자체가 사라진다. */
|
|
3272
|
+
consumeMaterials(t, taken) {
|
|
3273
|
+
if (!taken.length) return;
|
|
3274
|
+
const whole = [];
|
|
3275
|
+
for (const { epc, take } of taken) {
|
|
3276
|
+
const it = this.items.get(epc);
|
|
3277
|
+
if (!it) continue;
|
|
3278
|
+
const left = (it.qty ?? 1) - take;
|
|
3279
|
+
if (left > 0) {
|
|
3280
|
+
it.qty = left;
|
|
3281
|
+
this.recordMaterialActual(t, it.definitionId ?? it.gtin, "consumed", take, it.uom);
|
|
3282
|
+
this.emit(objectEvent({
|
|
3283
|
+
eventTime: this.now(),
|
|
3284
|
+
action: "OBSERVE",
|
|
3285
|
+
bizStep: CBV_BIZSTEP.consuming,
|
|
3286
|
+
disposition: DISP.in_progress,
|
|
3287
|
+
epcList: [],
|
|
3288
|
+
quantityList: [{ epcClass: it.gtin ?? it.epc, quantity: left, uom: it.uom }],
|
|
3289
|
+
readPoint: it.location,
|
|
3290
|
+
bizLocation: it.location
|
|
3291
|
+
}));
|
|
3292
|
+
continue;
|
|
3293
|
+
}
|
|
3294
|
+
this.recordMaterialActual(t, it.definitionId ?? it.gtin, "consumed", take, it.uom);
|
|
3295
|
+
whole.push(epc);
|
|
3296
|
+
}
|
|
3297
|
+
if (whole.length) {
|
|
3298
|
+
this.transform(whole, [], {
|
|
3299
|
+
bizStep: CBV_BIZSTEP.consuming,
|
|
3300
|
+
disposition: DISP.in_progress,
|
|
3301
|
+
transformationId: t.id,
|
|
3302
|
+
readPoint: this.items.get(whole[0])?.location ?? t.toNode
|
|
3303
|
+
});
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
2353
3306
|
claimPersonnel(t) {
|
|
2354
3307
|
const need = this.operationSpecs.get(t.kind)?.personnelSpecification;
|
|
2355
3308
|
if (!need?.length) return [];
|
|
@@ -2358,7 +3311,8 @@ var FlowEngine = class {
|
|
|
2358
3311
|
const want = Math.max(0, Math.floor(req.quantity ?? 0));
|
|
2359
3312
|
if (!want) continue;
|
|
2360
3313
|
const avail = [...this.persons.values()].filter(
|
|
2361
|
-
(p) => p.status === "idle" && !picked.includes(p.id) && !this.personOffShift(p) &&
|
|
3314
|
+
(p) => p.status === "idle" && !picked.includes(p.id) && !this.personOffShift(p) && !this.outOfEffect(p) && // 입사 전·퇴사 후는 이 시각의 모델에 없다
|
|
3315
|
+
/* 자격은 **상속을 타고 닫아** 판정한다 — 사람은 여러 등급에 속할 수 있고(표준), 등급은
|
|
2362
3316
|
다른 등급을 상속한다. "생산직 2명" 요구를 "용접 자격자" 가 만족해야 한다.
|
|
2363
3317
|
유효기간 밖의 등급은 닫힘에서 빠진다(만료된 자격으로 배정되지 않는다). */
|
|
2364
3318
|
(req.personnelClass === void 0 || classClosure(p.personnelClassIds, this.classDefs.personnel, this.now()).has(req.personnelClass))
|
|
@@ -2375,7 +3329,8 @@ var FlowEngine = class {
|
|
|
2375
3329
|
*/
|
|
2376
3330
|
claimEquipment(t) {
|
|
2377
3331
|
const free = (kind, picked2 = []) => [...this.equipment.values()].filter(
|
|
2378
|
-
(m) => m.status === "idle" && !m.held && !this.offShift(m) && !
|
|
3332
|
+
(m) => m.status === "idle" && !m.held && !this.offShift(m) && !this.outOfEffect(m) && // 도입 전·폐기 후는 이 시각의 모델에 없다
|
|
3333
|
+
!picked2.includes(m.id) && /* 설비의 소속은 아직 단수(`kind`)다 — 저널에 기록이 쌓여 복수화를 별도 작업으로 두었다.
|
|
2379
3334
|
다만 등급 정의가 있으면 **상속은 지금도 탄다**: 'welder-6axis' 가 'welder' 를 상속하면
|
|
2380
3335
|
'welder' 요구를 만족한다. 단수/복수와 상속은 다른 축이므로 하나를 기다리지 않는다. */
|
|
2381
3336
|
(kind === void 0 || classClosure(m.kind ? [m.kind] : [], this.classDefs.equipment, this.now()).has(kind))
|
|
@@ -2419,10 +3374,7 @@ var FlowEngine = class {
|
|
|
2419
3374
|
}
|
|
2420
3375
|
/** 사람의 교대 판정 — 자원(offShift)과 같은 규칙. */
|
|
2421
3376
|
personOffShift(p) {
|
|
2422
|
-
|
|
2423
|
-
if (!w) return false;
|
|
2424
|
-
const h = this.hourOfDay();
|
|
2425
|
-
return !(w.startHour <= w.endHour ? h >= w.startHour && h < w.endHour : h >= w.startHour || h < w.endHour);
|
|
3377
|
+
return this.offCalendar(p);
|
|
2426
3378
|
}
|
|
2427
3379
|
/**
|
|
2428
3380
|
* 이 자리가 동시 처리 한도에 찼는가 — `parallelism` 을 선언한 자리만 판정한다(미선언=제약 없음).
|
|
@@ -2438,14 +3390,71 @@ var FlowEngine = class {
|
|
|
2438
3390
|
}
|
|
2439
3391
|
/** 교대 밖인가 — 자원이 지금 일하지 않는 이유 중 고장·계획정지와 구별되는 세 번째. */
|
|
2440
3392
|
offShift(m) {
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
3393
|
+
return this.offCalendar(m);
|
|
3394
|
+
}
|
|
3395
|
+
/** 왜 쉬는가 — 휴일·휴게(non-working)인지 주말·교대 사이(off-hours)인지(§OffCalendarReason). */
|
|
3396
|
+
offReason(r) {
|
|
3397
|
+
const why = offCalendarReasonAt(r, this.nowMs(), this.boardDef?.utcOffsetMinutes);
|
|
3398
|
+
return why ? { offShiftReason: why } : {};
|
|
3399
|
+
}
|
|
3400
|
+
/**
|
|
3401
|
+
* 유효 기간 밖인가 — **네 번째 이유**(§Effectivity). 설비·사람·자산이 같은 규칙을 쓴다.
|
|
3402
|
+
*
|
|
3403
|
+
* 시뮬 시각을 ISO 로 풀어 계약의 `effectivityAt` 에 넘긴다. **판정은 커널에 두지 않는다** —
|
|
3404
|
+
* 호스트(라이브 관측)도 같은 판정을 해야 하고, 규칙이 두 벌이면 갈라진다.
|
|
3405
|
+
*/
|
|
3406
|
+
effectivityOf(r) {
|
|
3407
|
+
if (!r.effectiveStart && !r.effectiveEnd) return void 0;
|
|
3408
|
+
return effectivityAt(r, this.now());
|
|
3409
|
+
}
|
|
3410
|
+
/**
|
|
3411
|
+
* 지금 어느 교대인가 — 계약의 `activeShiftOf` 에 위임한다(미러도 같은 함수를 쓴다).
|
|
3412
|
+
* 선언이 없으면 `undefined`: 교대를 나눠 놓지 않은 현장에 이름을 지어내지 않는다.
|
|
3413
|
+
*/
|
|
3414
|
+
shiftOf(r) {
|
|
3415
|
+
if (!r.workCalendar?.length) return void 0;
|
|
3416
|
+
return activeShiftAt(r.workCalendar, this.nowMs(), this.boardDef?.utcOffsetMinutes);
|
|
3417
|
+
}
|
|
3418
|
+
/** 유효 기간 밖이라 이 시각의 모델에 참여하지 않는가 — 배정 게이트가 쓰는 형태. */
|
|
3419
|
+
outOfEffect(r) {
|
|
3420
|
+
return this.effectivityOf(r) !== void 0;
|
|
3421
|
+
}
|
|
3422
|
+
/**
|
|
3423
|
+
* 스냅샷·델타에 실을 조각 — **선언한 기간과 판정을 함께** 낸다.
|
|
3424
|
+
*
|
|
3425
|
+
* 판정만 내면 화면이 "왜" 를 말할 수 없고(언제 폐기됐나), 기간만 내면 소비처마다 다시 판정해 갈라진다.
|
|
3426
|
+
* 선언이 없으면 아무것도 붙이지 않는다(대부분의 자원이 그렇다 — 필드를 늘리지 않는다).
|
|
3427
|
+
*/
|
|
3428
|
+
effectivePart(r) {
|
|
3429
|
+
if (!r.effectiveStart && !r.effectiveEnd) return {};
|
|
3430
|
+
const e = this.effectivityOf(r);
|
|
3431
|
+
return {
|
|
3432
|
+
...r.effectiveStart ? { effectiveStart: r.effectiveStart } : {},
|
|
3433
|
+
...r.effectiveEnd ? { effectiveEnd: r.effectiveEnd } : {},
|
|
3434
|
+
...e ? { effectivity: e } : {}
|
|
3435
|
+
};
|
|
3436
|
+
}
|
|
3437
|
+
/**
|
|
3438
|
+
* 지금 근무 시간 밖인가 — **캘린더가 있으면 그것으로, 없으면 옛 `window` 로** 판정한다.
|
|
3439
|
+
*
|
|
3440
|
+
* `window`(시 단위 하나)는 캘린더의 특수한 경우다. 둘을 한 함수로 모아 두면 인원·설비가 같은 규칙을
|
|
3441
|
+
* 쓴다(예전에는 같은 식이 두 곳에 복사돼 있었다 — 한쪽만 고치면 조용히 갈라진다).
|
|
3442
|
+
*/
|
|
3443
|
+
offCalendar(r) {
|
|
3444
|
+
return offCalendarAt(r, this.nowMs(), this.boardDef?.utcOffsetMinutes);
|
|
3445
|
+
}
|
|
3446
|
+
/**
|
|
3447
|
+
* 시뮬 시각의 분(0..1439) — 캘린더 판정의 기준.
|
|
3448
|
+
*
|
|
3449
|
+
* **보드가 선언한 기준으로 읽는다**(`utcOffsetMinutes`). 예전에는 UTC 로 읽어서 Rosarito(UTC−7)의
|
|
3450
|
+
* 06시 교대가 7시간 틀렸다 — 시각대만 적고 기준을 안 적으면 반드시 이렇게 된다.
|
|
3451
|
+
*/
|
|
3452
|
+
minuteOfDay() {
|
|
3453
|
+
return minuteOfDayAt(this.nowMs(), this.boardDef?.utcOffsetMinutes);
|
|
2445
3454
|
}
|
|
2446
3455
|
/** 시뮬 시각의 시(0..23) — 운영시간·시간대 배율의 기준. */
|
|
2447
3456
|
hourOfDay() {
|
|
2448
|
-
return new Date(
|
|
3457
|
+
return new Date(this.nowMs()).getUTCHours();
|
|
2449
3458
|
}
|
|
2450
3459
|
/**
|
|
2451
3460
|
* 운영시간 안인가 — `window {startHour, endHour}`. **선언만 되고 소비처가 없던 필드**를 판정한다.
|
|
@@ -2505,6 +3514,8 @@ var FlowEngine = class {
|
|
|
2505
3514
|
const gtinKey = parsedClass?.gtinKey ?? parsedSelf.gtinKey;
|
|
2506
3515
|
return {
|
|
2507
3516
|
epc: i.epc,
|
|
3517
|
+
...i.subLotId ? { subLotId: i.subLotId } : {},
|
|
3518
|
+
...i.definitionId ? { definitionId: i.definitionId } : {},
|
|
2508
3519
|
...i.gtin ? { gtin: i.gtin } : {},
|
|
2509
3520
|
...gtinKey ? { gtinKey } : {},
|
|
2510
3521
|
...lot ? { lot } : {},
|
|
@@ -2514,6 +3525,7 @@ var FlowEngine = class {
|
|
|
2514
3525
|
...i.carriedBy ? { carriedBy: i.carriedBy } : {},
|
|
2515
3526
|
...i.qty !== void 0 ? { qty: i.qty } : {},
|
|
2516
3527
|
...i.uom ? { uom: i.uom } : {},
|
|
3528
|
+
...i.quantities?.length ? { quantities: i.quantities } : {},
|
|
2517
3529
|
...i.expiry !== void 0 ? { expiry: i.expiry } : {},
|
|
2518
3530
|
...i.ilmd ? { ilmd: i.ilmd } : {}
|
|
2519
3531
|
};
|
|
@@ -2541,7 +3553,9 @@ var FlowEngine = class {
|
|
|
2541
3553
|
}
|
|
2542
3554
|
}
|
|
2543
3555
|
processOrders() {
|
|
2544
|
-
|
|
3556
|
+
const pending = [...this.orders.values()].filter((o) => o.status === "created" && !o.held);
|
|
3557
|
+
if (pending.some((o) => o.priority !== void 0)) pending.sort((a, b) => priorityRank(a.priority) - priorityRank(b.priority));
|
|
3558
|
+
for (const o of pending) this.allocate(o);
|
|
2545
3559
|
}
|
|
2546
3560
|
/**
|
|
2547
3561
|
* 작업 진행 — **진행을 먼저, 배정을 나중에.**
|
|
@@ -2555,18 +3569,23 @@ var FlowEngine = class {
|
|
|
2555
3569
|
this.assignTasks();
|
|
2556
3570
|
}
|
|
2557
3571
|
assignTasks() {
|
|
2558
|
-
|
|
3572
|
+
const queue = [...this.tasks.values()];
|
|
3573
|
+
if (queue.some((t) => t.priority !== void 0)) queue.sort((a, b) => priorityRank(a.priority) - priorityRank(b.priority));
|
|
3574
|
+
for (const t of queue) {
|
|
2559
3575
|
if (t.status !== "created") continue;
|
|
2560
3576
|
const crew = this.claimPersonnel(t);
|
|
2561
3577
|
if (crew === null) continue;
|
|
2562
3578
|
const gear = this.claimAssets(t);
|
|
2563
3579
|
if (gear === null) continue;
|
|
3580
|
+
const stock = this.claimMaterials(t);
|
|
3581
|
+
if (stock === null) continue;
|
|
2564
3582
|
if (t.intent === "dwell") {
|
|
2565
3583
|
t.status = "in-progress";
|
|
2566
3584
|
t.remainingMs = t.durationMs;
|
|
2567
3585
|
t.startedAtSimMs = this.clockMs;
|
|
2568
3586
|
this.assignCrew(t, crew);
|
|
2569
3587
|
this.assignAssets(t, gear);
|
|
3588
|
+
this.consumeMaterials(t, stock);
|
|
2570
3589
|
this.emitTask(t);
|
|
2571
3590
|
continue;
|
|
2572
3591
|
}
|
|
@@ -2591,6 +3610,7 @@ var FlowEngine = class {
|
|
|
2591
3610
|
if (rigs.length > 1) t.resources = rigs.slice();
|
|
2592
3611
|
this.assignCrew(t, crew);
|
|
2593
3612
|
this.assignAssets(t, gear);
|
|
3613
|
+
this.consumeMaterials(t, stock);
|
|
2594
3614
|
this.emitTask(t);
|
|
2595
3615
|
if (t.intent === "process") this.emitEquipment(eq);
|
|
2596
3616
|
else this.emitEquipment(eq, { fromNode: t.fromNode, toNode: t.toNode, startedAtSimMs: this.clockMs, durationMs: t.durationMs, progress: 0, elapsedMs: 0 });
|
|
@@ -2603,6 +3623,33 @@ var FlowEngine = class {
|
|
|
2603
3623
|
if (t.resource && this.equipment.get(t.resource)?.status === "down") continue;
|
|
2604
3624
|
t.remainingMs -= dt;
|
|
2605
3625
|
if (t.remainingMs > 0) continue;
|
|
3626
|
+
const missing = this.missingContextOf(t);
|
|
3627
|
+
if (missing) {
|
|
3628
|
+
this.abandonedTasks++;
|
|
3629
|
+
if (this.abandonedTasks === 1) {
|
|
3630
|
+
const why = missing === "item" ? "the item it moves is no longer in state (consumed, shipped, or the observation did not carry it)" : missing === "order" ? "the order it belongs to is no longer in state (already fulfilled, so the seed did not restore it)" : "the domain cannot complete it (its order or product definition is missing)";
|
|
3631
|
+
console.warn(`[twin-kernel] "${this.tenantId}": folding task ${t.id} \u2014 ${why}. Further such tasks are counted, not logged.`);
|
|
3632
|
+
}
|
|
3633
|
+
this.tasks.delete(t.id);
|
|
3634
|
+
this.releaseCrew(t);
|
|
3635
|
+
this.releaseAssets(t);
|
|
3636
|
+
if (t.resource) {
|
|
3637
|
+
const rig = this.equipment.get(t.resource);
|
|
3638
|
+
if (rig) {
|
|
3639
|
+
rig.status = "idle";
|
|
3640
|
+
rig.taskId = null;
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
for (const id of t.resources ?? []) {
|
|
3644
|
+
const m = this.equipment.get(id);
|
|
3645
|
+
if (m) {
|
|
3646
|
+
m.status = "idle";
|
|
3647
|
+
m.taskId = null;
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
continue;
|
|
3651
|
+
}
|
|
3652
|
+
this.produceMaterials(t);
|
|
2606
3653
|
this.onTaskComplete(t);
|
|
2607
3654
|
t.status = "completed";
|
|
2608
3655
|
this.releaseCrew(t);
|
|
@@ -2651,6 +3698,7 @@ var WmsKernel = class extends FlowEngine {
|
|
|
2651
3698
|
if (!dock) return;
|
|
2652
3699
|
const epc = ssccUri(COMPANY_PREFIX, ++this.epcSeq);
|
|
2653
3700
|
const gtin = this.pickGtin(spec.content.skuMix);
|
|
3701
|
+
if (!gtin) return;
|
|
2654
3702
|
const qty = this.randInt(spec.content.qtyPerLine.min, spec.content.qtyPerLine.max);
|
|
2655
3703
|
const po = gdtiUri(COMPANY_PREFIX, "401", ++this.poSeq);
|
|
2656
3704
|
const eventTime = this.now();
|
|
@@ -2688,7 +3736,9 @@ var WmsKernel = class extends FlowEngine {
|
|
|
2688
3736
|
onOrder(spec) {
|
|
2689
3737
|
const lpl = spec.content.linesPerOrder;
|
|
2690
3738
|
if (!lpl) {
|
|
2691
|
-
|
|
3739
|
+
const only = this.pickGtin(spec.content.skuMix);
|
|
3740
|
+
if (!only) return;
|
|
3741
|
+
this.createSalesOrder([{ gtin: only, qty: this.randInt(spec.content.qtyPerLine.min, spec.content.qtyPerLine.max) }], spec);
|
|
2692
3742
|
return;
|
|
2693
3743
|
}
|
|
2694
3744
|
const n = Math.min(this.randInt(lpl.min, lpl.max), spec.content.skuMix.length);
|
|
@@ -2696,10 +3746,12 @@ var WmsKernel = class extends FlowEngine {
|
|
|
2696
3746
|
let pool = spec.content.skuMix;
|
|
2697
3747
|
for (let i = 0; i < n && pool.length > 0; i++) {
|
|
2698
3748
|
const gtin = this.pickGtin(pool);
|
|
3749
|
+
if (!gtin) break;
|
|
2699
3750
|
lines.push({ gtin, qty: this.randInt(spec.content.qtyPerLine.min, spec.content.qtyPerLine.max) });
|
|
2700
3751
|
pool = pool.filter((m) => m.gtin !== gtin);
|
|
2701
3752
|
}
|
|
2702
|
-
|
|
3753
|
+
if (!lines.length) return;
|
|
3754
|
+
this.createSalesOrder(lines, spec);
|
|
2703
3755
|
}
|
|
2704
3756
|
/** Command 채널 — 오더 즉시 투입(트랜잭션 프론트엔드/prescriptive). */
|
|
2705
3757
|
handleCommand(cmd) {
|
|
@@ -2712,7 +3764,12 @@ var WmsKernel = class extends FlowEngine {
|
|
|
2712
3764
|
}
|
|
2713
3765
|
return super.handleCommand(cmd);
|
|
2714
3766
|
}
|
|
2715
|
-
|
|
3767
|
+
/**
|
|
3768
|
+
* 오더 생성 — 약속(`promised`)이 있으면 **납기와 우선순위를 함께 싣는다**(표준 `EndTime`·`Priority`).
|
|
3769
|
+
* 없으면 싣지 않는다: 없는 약속을 만들면 그 뒤 모든 지연 판정이 거짓 위에 선다.
|
|
3770
|
+
*/
|
|
3771
|
+
/** 오더 생성 — 약속(납기·우선순위)은 base `promiseOf` 가 계산한다(도메인마다 다르게 재지 않는다). */
|
|
3772
|
+
createSalesOrder(lines, spec) {
|
|
2716
3773
|
const id = `order-${++this.orderSeq}`;
|
|
2717
3774
|
const so = gdtiUri(COMPANY_PREFIX, "402", ++this.soSeq);
|
|
2718
3775
|
const requested = lines.reduce((s, l) => s + l.qty, 0);
|
|
@@ -2726,7 +3783,8 @@ var WmsKernel = class extends FlowEngine {
|
|
|
2726
3783
|
allocated: [],
|
|
2727
3784
|
picked: [],
|
|
2728
3785
|
shipmentEpc: null,
|
|
2729
|
-
lines: lines.map((l) => ({ gtin: l.gtin, requested: l.qty }))
|
|
3786
|
+
lines: lines.map((l) => ({ gtin: l.gtin, requested: l.qty })),
|
|
3787
|
+
...spec ? this.promiseOf(spec) : {}
|
|
2730
3788
|
};
|
|
2731
3789
|
this.orders.set(id, order);
|
|
2732
3790
|
this.emitOrder(order);
|
|
@@ -2766,7 +3824,8 @@ var WmsKernel = class extends FlowEngine {
|
|
|
2766
3824
|
}
|
|
2767
3825
|
/** 태스크 완료 — 이동 반영 후 putaway=storing, pick=picking(+전량 시 pack→stage→ship). */
|
|
2768
3826
|
onTaskComplete(t) {
|
|
2769
|
-
const item = this.
|
|
3827
|
+
const item = this.itemByRef(t.itemEpc);
|
|
3828
|
+
if (!item) throw new Error(`task ${t.id}: item "${t.itemEpc}" vanished between the core check and the domain hook`);
|
|
2770
3829
|
const from = this.locations.get(t.fromNode);
|
|
2771
3830
|
const to = this.locations.get(t.toNode);
|
|
2772
3831
|
from.occupancy--;
|
|
@@ -2819,6 +3878,7 @@ var WmsKernel = class extends FlowEngine {
|
|
|
2819
3878
|
var TRAVEL_MS2 = 2e4;
|
|
2820
3879
|
var DWELL_MS = 3e4;
|
|
2821
3880
|
var WINDOW_DELAY_MS = 4e4;
|
|
3881
|
+
var WINDOW_LENGTH_MS = 20 * 6e4;
|
|
2822
3882
|
var CARGO_PER_TRAILER = 2;
|
|
2823
3883
|
var CP = "0614141";
|
|
2824
3884
|
var YmsKernel = class extends FlowEngine {
|
|
@@ -2836,14 +3896,14 @@ var YmsKernel = class extends FlowEngine {
|
|
|
2836
3896
|
}
|
|
2837
3897
|
/** 인바운드(하차) 자극 — 화물 적재된 트레일러 도착. */
|
|
2838
3898
|
onArrival(_spec) {
|
|
2839
|
-
this.spawnAppointment("appointment");
|
|
3899
|
+
this.spawnAppointment("appointment", _spec);
|
|
2840
3900
|
}
|
|
2841
3901
|
/** 아웃바운드(상차) 자극 — 빈 트레일러 도착(도크에서 staging 화물 적재 예정). */
|
|
2842
3902
|
onOrder(_spec) {
|
|
2843
|
-
this.spawnAppointment("appointment-out");
|
|
3903
|
+
this.spawnAppointment("appointment-out", _spec);
|
|
2844
3904
|
}
|
|
2845
3905
|
/** 게이트-인 — 트레일러 도착 → 어포인트먼트(도크도어 배정 + 창). drop 은 야드 주차 태스크, live 는 게이트 대기. */
|
|
2846
|
-
spawnAppointment(kind) {
|
|
3906
|
+
spawnAppointment(kind, spec) {
|
|
2847
3907
|
const gate = this.locationByType("gate");
|
|
2848
3908
|
const doors = [...this.locations.values()].filter((n) => n.type === "dock-door").sort((a, b) => a.id.localeCompare(b.id));
|
|
2849
3909
|
if (!gate || doors.length === 0) return;
|
|
@@ -2860,7 +3920,24 @@ var YmsKernel = class extends FlowEngine {
|
|
|
2860
3920
|
const door = doors[this.doorRR++ % doors.length];
|
|
2861
3921
|
const id = `order-${++this.orderSeq}`;
|
|
2862
3922
|
const appt = gdtiUri(CP, "404", ++this.soSeq);
|
|
2863
|
-
const order = {
|
|
3923
|
+
const order = {
|
|
3924
|
+
id,
|
|
3925
|
+
kind,
|
|
3926
|
+
status: "created",
|
|
3927
|
+
requested: 1,
|
|
3928
|
+
fulfilled: 0,
|
|
3929
|
+
bizTransaction: appt,
|
|
3930
|
+
allocated: [epc],
|
|
3931
|
+
picked: [],
|
|
3932
|
+
dockDoor: door.id,
|
|
3933
|
+
/* 내부 스케줄 게이트(시뮬 ms) — 표준 `EarliestStartTime` 과 같은 개념의 우리 단위. */
|
|
3934
|
+
windowStartMs: this.clockMs + WINDOW_DELAY_MS,
|
|
3935
|
+
/* 표준 필드로도 함께 낸다 — 소비처(화면·집계·미러)는 표준 축으로 읽는다.
|
|
3936
|
+
`startTime` = 창이 열리는 때 · `endTime` = 창이 닫히는 때(= 납기). */
|
|
3937
|
+
startTime: new Date(Date.parse(this.now()) + WINDOW_DELAY_MS).toISOString(),
|
|
3938
|
+
endTime: new Date(Date.parse(this.now()) + WINDOW_DELAY_MS + WINDOW_LENGTH_MS).toISOString(),
|
|
3939
|
+
...spec?.priority !== void 0 ? { priority: spec.priority } : {}
|
|
3940
|
+
};
|
|
2864
3941
|
this.orders.set(id, order);
|
|
2865
3942
|
this.apptMode.set(id, this.mode);
|
|
2866
3943
|
this.emitOrder(order);
|
|
@@ -2907,7 +3984,8 @@ var YmsKernel = class extends FlowEngine {
|
|
|
2907
3984
|
}
|
|
2908
3985
|
/** 태스크 완료 — 목적지 타입으로 분기: yard-slot=주차, dock-door=상/하차+depart, gate=출차. */
|
|
2909
3986
|
onTaskComplete(t) {
|
|
2910
|
-
const trailer = this.
|
|
3987
|
+
const trailer = this.itemByRef(t.itemEpc);
|
|
3988
|
+
if (!trailer) throw new Error(`task ${t.id}: item "${t.itemEpc}" vanished between the core check and the domain hook`);
|
|
2911
3989
|
const from = this.locations.get(t.fromNode);
|
|
2912
3990
|
const to = this.locations.get(t.toNode);
|
|
2913
3991
|
from.occupancy--;
|
|
@@ -2993,7 +4071,29 @@ var MesKernel = class extends FlowEngine {
|
|
|
2993
4071
|
constructor(tenantId, policy = firstFitPolicy, mesSpec) {
|
|
2994
4072
|
super(tenantId, policy);
|
|
2995
4073
|
this.mesSpec = mesSpec;
|
|
2996
|
-
if (mesSpec?.definition?.operations)
|
|
4074
|
+
if (mesSpec?.definition?.operations) {
|
|
4075
|
+
this.loadOperations(mesSpec.definition.operations);
|
|
4076
|
+
this.assertNoDoubleProduction(mesSpec.definition.operations);
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
4079
|
+
/**
|
|
4080
|
+
* **산출을 두 곳에서 만들지 않는다** — 기동 때 막는다.
|
|
4081
|
+
*
|
|
4082
|
+
* MES 는 레시피로 산출을 만든다: WIP 사슬(스텝마다 앞의 WIP 을 먹고 다음 WIP 을 낸다) · **직렬번호**
|
|
4083
|
+
* (회사 프리픽스 + 일련번호) · **수율**(양품/불량이 처분을 가른다) · 생산오더 연결. 코어의 일반
|
|
4084
|
+
* 자재 명세(`use: 'produced'`)는 **비직렬 클래스+수량**이라 그 넷을 표현하지 못한다.
|
|
4085
|
+
*
|
|
4086
|
+
* 그래서 MES 를 일반 선언으로 옮기지 않는다 — 옮기면 정체성·수율·오더 연결을 **잃는다**(§4-20).
|
|
4087
|
+
* 대신 둘이 겹치면 **같은 산출을 두 번 만들게** 되므로, 그 조합을 기동 시점에 거부한다.
|
|
4088
|
+
* 런타임에 조용히 두 배가 되는 것보다 기동이 실패하는 편이 낫다(재고가 거짓이 되면 그 위의
|
|
4089
|
+
* 모든 계산이 거짓이 된다).
|
|
4090
|
+
*/
|
|
4091
|
+
assertNoDoubleProduction(ops) {
|
|
4092
|
+
const bad = ops.filter((o) => (o.materialSpecification ?? []).some((m) => m.use === "produced")).map((o) => o.key);
|
|
4093
|
+
if (!bad.length) return;
|
|
4094
|
+
throw new Error(
|
|
4095
|
+
`MES recipe already produces outputs \u2014 operations [${bad.join(", ")}] must not also declare materialSpecification use:'produced' (that would create the same output twice). Consumption specs are fine; declare production in the recipe.`
|
|
4096
|
+
);
|
|
2997
4097
|
}
|
|
2998
4098
|
productOf(gtin) {
|
|
2999
4099
|
return PRODUCTS.find((p) => p.gtin === gtin);
|
|
@@ -3038,7 +4138,7 @@ var MesKernel = class extends FlowEngine {
|
|
|
3038
4138
|
const product = PRODUCTS[this.orderSeq % PRODUCTS.length];
|
|
3039
4139
|
const id = `order-${++this.orderSeq}`;
|
|
3040
4140
|
const wo = gdtiUri(CP2, "403", ++this.soSeq);
|
|
3041
|
-
const order = { id, kind: "workorder", status: "created", gtin: product.gtin, requested: 1, fulfilled: 0, bizTransaction: wo, allocated: [], picked: [] };
|
|
4141
|
+
const order = { id, kind: "workorder", status: "created", gtin: product.gtin, requested: 1, fulfilled: 0, bizTransaction: wo, allocated: [], picked: [], ...this.promiseOf(_spec) };
|
|
3042
4142
|
this.orders.set(id, order);
|
|
3043
4143
|
this.emitOrder(order);
|
|
3044
4144
|
}
|
|
@@ -3071,10 +4171,23 @@ var MesKernel = class extends FlowEngine {
|
|
|
3071
4171
|
this.emitTask(task);
|
|
3072
4172
|
}
|
|
3073
4173
|
/** op 완료 = 변환. 라우트 인덱스로 분기: 첫=BOM 소비→WIP, 중간=WIP→WIP, 마지막(조립)=WIP→완성차(수율→OEE 품질). */
|
|
4174
|
+
/**
|
|
4175
|
+
* MES 는 완료 시점에 **오더와 제품 정의**를 딛고 선다 — 둘 중 하나만 없어도 끝맺을 수 없다.
|
|
4176
|
+
*
|
|
4177
|
+
* 씨앗이 이미 이행된 오더를 심지 않으므로(남은 수량 0) 그 오더에 딸린 작업만 남을 수 있고,
|
|
4178
|
+
* 관측이 오더 연결을 담지 못한 작업도 있다. 그때 예전에는 `order.gtin` 에서 던졌고 **그 예외
|
|
4179
|
+
* 하나가 정확도 추세 전체를 죽였다.** 이제 코어가 이 답을 보고 그 작업만 접는다.
|
|
4180
|
+
*/
|
|
4181
|
+
canComplete(t) {
|
|
4182
|
+
if (this.mesSpec) return !!(t.orderId && this.orders.get(t.orderId));
|
|
4183
|
+
const order = t.orderId ? this.orders.get(t.orderId) : void 0;
|
|
4184
|
+
return !!order && !!this.productOf(order.gtin);
|
|
4185
|
+
}
|
|
3074
4186
|
onTaskComplete(t) {
|
|
3075
4187
|
if (this.mesSpec) return this.onTaskCompleteDef(t);
|
|
3076
4188
|
const order = this.orders.get(t.orderId);
|
|
3077
|
-
const product = this.productOf(order.gtin);
|
|
4189
|
+
const product = order && this.productOf(order.gtin);
|
|
4190
|
+
if (!order || !product) throw new Error(`task ${t.id}: order/product vanished between the core check and the domain hook`);
|
|
3078
4191
|
const i = ROUTE.findIndex((s) => s.kind === t.kind);
|
|
3079
4192
|
const loc = this.locations.get(t.toNode);
|
|
3080
4193
|
const isLast = i === ROUTE.length - 1;
|
|
@@ -3114,6 +4227,17 @@ var MesKernel = class extends FlowEngine {
|
|
|
3114
4227
|
serialOf(materialKey, serial) {
|
|
3115
4228
|
return sgtinUri(this.mesSpec.companyPrefix, this.mesSpec.binding[materialKey], serial);
|
|
3116
4229
|
}
|
|
4230
|
+
/**
|
|
4231
|
+
* 라우트를 용량 계산에 알려 준다 — 수율을 거슬러 올릴 때 순서가 곧 계산이다.
|
|
4232
|
+
*
|
|
4233
|
+
* 순서를 모르면 선언 순서대로 세는데, 그러면 하류에서 잃는 몫이 엉뚱한 공정에 얹힌다(조용히
|
|
4234
|
+
* 틀린다). 생산 정의를 가진 커널만 이 답을 안다.
|
|
4235
|
+
*/
|
|
4236
|
+
routeKeys() {
|
|
4237
|
+
if (!this.mesSpec) return void 0;
|
|
4238
|
+
const d = this.mesSpec.definition;
|
|
4239
|
+
return d.routes?.find((r) => r.key === this.recipeDef().route)?.steps;
|
|
4240
|
+
}
|
|
3117
4241
|
/** recipe.route → 오퍼레이션 시퀀스 해소. */
|
|
3118
4242
|
routeOps() {
|
|
3119
4243
|
const d = this.mesSpec.definition;
|
|
@@ -3125,6 +4249,7 @@ var MesKernel = class extends FlowEngine {
|
|
|
3125
4249
|
const rawStore = this.locationByType("raw-store");
|
|
3126
4250
|
if (!rawStore) return;
|
|
3127
4251
|
const gtin = this.pickGtin(spec.content.skuMix);
|
|
4252
|
+
if (!gtin) return;
|
|
3128
4253
|
const inputKey = this.recipeDef().inputs.map((i) => i.material).find((k) => this.classOf(k) === gtin);
|
|
3129
4254
|
if (!inputKey) return;
|
|
3130
4255
|
const epc = this.serialOf(inputKey, ++this.epcSeq);
|
|
@@ -3137,7 +4262,7 @@ var MesKernel = class extends FlowEngine {
|
|
|
3137
4262
|
const rc = this.recipeDef();
|
|
3138
4263
|
const id = `order-${++this.orderSeq}`;
|
|
3139
4264
|
const wo = gdtiUri(this.mesSpec.companyPrefix, "403", ++this.soSeq);
|
|
3140
|
-
const order = { id, kind: "workorder", status: "created", gtin: this.classOf(rc.outputs[0].material), requested: 1, fulfilled: 0, bizTransaction: wo, allocated: [], picked: [] };
|
|
4265
|
+
const order = { id, kind: "workorder", status: "created", gtin: this.classOf(rc.outputs[0].material), requested: 1, fulfilled: 0, bizTransaction: wo, allocated: [], picked: [], ...this.promiseOf(_spec) };
|
|
3141
4266
|
this.orders.set(id, order);
|
|
3142
4267
|
this.emitOrder(order);
|
|
3143
4268
|
}
|
|
@@ -3170,6 +4295,7 @@ var MesKernel = class extends FlowEngine {
|
|
|
3170
4295
|
/** 정의 모드 완료 — 라우트 인덱스: 중간=WIP 변환+다음 스텝, 마지막=완제품(수율). */
|
|
3171
4296
|
onTaskCompleteDef(t) {
|
|
3172
4297
|
const order = this.orders.get(t.orderId);
|
|
4298
|
+
if (!order) throw new Error(`task ${t.id}: order vanished between the core check and the domain hook`);
|
|
3173
4299
|
const rc = this.recipeDef();
|
|
3174
4300
|
const ops = this.routeOps();
|
|
3175
4301
|
const i = ops.findIndex((s) => s.key === t.kind);
|
|
@@ -3213,6 +4339,12 @@ var VOCABULARY_EXCEPTIONS = [
|
|
|
3213
4339
|
{ token: "moverId", why: "journal wire field \u2014 renaming would mix two keys for one fact across history" },
|
|
3214
4340
|
{ token: "fromNode", why: "journal wire field (task.status payload)" },
|
|
3215
4341
|
{ token: "toNode", why: "journal wire field (task.status payload)" },
|
|
4342
|
+
/* ── 보드의 옛 세대 키 — 저장된 데이터라 읽어는 줘야 한다 ────────────────
|
|
4343
|
+
* 설비 배열의 이름은 세 세대를 거쳤다(movers → equipmentList → equipment). 저장된 보드에는
|
|
4344
|
+
* 셋이 섞여 있고(실측 23개 중 13개가 `movers`), 하나라도 안 읽으면 그 보드는 **설비가 0인 공장**
|
|
4345
|
+
* 으로 조용히 읽힌다 — 화면의 설비 수가 0이 되고 용량 판정에서 자원이 사라진다. 쓰는 곳은
|
|
4346
|
+
* `readBoardEquipment` 한 곳뿐이고, 거기서 새 이름으로 정규화해 내보낸다. */
|
|
4347
|
+
{ token: "movers", why: "legacy board key (movers \u2192 equipmentList \u2192 equipment); read-only normalization in readBoardEquipment, 13 stored boards still use it" },
|
|
3216
4348
|
/* ── 씬 컴포넌트 타입 — 보드에 저장된 값이고, 뜻이 어긋나지도 않는다 ──────
|
|
3217
4349
|
* 보드 7개가 이 타입으로 컴포넌트를 담고 있어 개명하면 그 컴포넌트가 조용히 안 그려진다.
|
|
3218
4350
|
* 그리고 씬에서 이 이름은 자원이 아니라 **움직임 표현**을 가리킨다(표준과 충돌 아님). */
|
|
@@ -3248,6 +4380,7 @@ function retiredVocabularyIn(line) {
|
|
|
3248
4380
|
BTT_PRODORDER,
|
|
3249
4381
|
CAPABILITIES,
|
|
3250
4382
|
CAPABILITY_KEYS,
|
|
4383
|
+
CBV_BIZSTEP,
|
|
3251
4384
|
CMD,
|
|
3252
4385
|
DISP,
|
|
3253
4386
|
DOMAIN_CATALOG,
|
|
@@ -3259,6 +4392,7 @@ function retiredVocabularyIn(line) {
|
|
|
3259
4392
|
GUARD_PRAGMA,
|
|
3260
4393
|
ILMD_ATTR,
|
|
3261
4394
|
LOCATION_SATURATION_NEAR,
|
|
4395
|
+
MATERIAL_PROPERTY,
|
|
3262
4396
|
MES_BIZSTEP,
|
|
3263
4397
|
MES_LOCATION_TYPES,
|
|
3264
4398
|
MES_PART_GTINS,
|
|
@@ -3269,6 +4403,7 @@ function retiredVocabularyIn(line) {
|
|
|
3269
4403
|
OP_EVENT,
|
|
3270
4404
|
OP_PARAM,
|
|
3271
4405
|
ObservedReducer,
|
|
4406
|
+
PRIORITY_UNSET,
|
|
3272
4407
|
RETIRED_VOCABULARY,
|
|
3273
4408
|
StateProjector,
|
|
3274
4409
|
TwinHistory,
|
|
@@ -3283,41 +4418,61 @@ function retiredVocabularyIn(line) {
|
|
|
3283
4418
|
YMS_LOCATION_TYPES,
|
|
3284
4419
|
YMS_TYPES,
|
|
3285
4420
|
YmsKernel,
|
|
4421
|
+
activeShiftAt,
|
|
4422
|
+
activeShiftOf,
|
|
3286
4423
|
aggregationEvent,
|
|
4424
|
+
analyzeCapacity,
|
|
3287
4425
|
capabilitiesForType,
|
|
3288
4426
|
classClosure,
|
|
3289
4427
|
compareStates,
|
|
3290
4428
|
computeOee,
|
|
3291
4429
|
constantDuration,
|
|
4430
|
+
conversionFactorOf,
|
|
3292
4431
|
counterfactualAt,
|
|
3293
4432
|
deriveAttentions,
|
|
4433
|
+
dueStatusOf,
|
|
4434
|
+
effectivityAt,
|
|
3294
4435
|
fefoPolicy,
|
|
3295
4436
|
firstFitPolicy,
|
|
4437
|
+
foldJobResponses,
|
|
3296
4438
|
foldTaskRecords,
|
|
3297
4439
|
gdtiUri,
|
|
3298
4440
|
graiUri,
|
|
3299
4441
|
hierarchyOf,
|
|
4442
|
+
inWorkCalendar,
|
|
4443
|
+
inWorkCalendarAt,
|
|
3300
4444
|
ingest,
|
|
3301
4445
|
isEquipmentLevel,
|
|
4446
|
+
isoDurationHours,
|
|
4447
|
+
itemKeyOf,
|
|
3302
4448
|
lgtinClass,
|
|
3303
4449
|
locationStatusOf,
|
|
3304
4450
|
mapRecord,
|
|
4451
|
+
minuteOfDayAt,
|
|
3305
4452
|
monteCarloForecast,
|
|
3306
4453
|
objectEvent,
|
|
4454
|
+
offCalendarAt,
|
|
4455
|
+
offCalendarReasonAt,
|
|
3307
4456
|
parseEpc,
|
|
3308
4457
|
parseIsoDuration,
|
|
3309
4458
|
partialFitPolicy,
|
|
4459
|
+
priorityRank,
|
|
4460
|
+
quantityIn,
|
|
3310
4461
|
readBoardAssets,
|
|
3311
4462
|
readBoardEquipment,
|
|
3312
4463
|
readBoardLocations,
|
|
3313
4464
|
replay,
|
|
4465
|
+
replaySegments,
|
|
3314
4466
|
retiredVocabularyIn,
|
|
3315
4467
|
sgtinClass,
|
|
3316
4468
|
sgtinUri,
|
|
3317
4469
|
ssccUri,
|
|
3318
4470
|
stateFieldsOf,
|
|
4471
|
+
subLotIdOf,
|
|
3319
4472
|
transactionEvent,
|
|
3320
4473
|
transformationEvent,
|
|
3321
4474
|
validateDomainDefinition,
|
|
3322
|
-
validateEpcisEvent
|
|
4475
|
+
validateEpcisEvent,
|
|
4476
|
+
weekdayAt,
|
|
4477
|
+
workingTimeOfWeek
|
|
3323
4478
|
});
|