@operato/twin-kernel 0.2.3 → 0.4.0
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/README.md +46 -5
- package/dist/capability.js +2 -2
- package/dist/capacity.d.ts +99 -0
- package/dist/capacity.js +172 -0
- package/dist/contract.d.ts +836 -45
- package/dist/contract.js +519 -7
- package/dist/divergence.d.ts +1 -1
- package/dist/divergence.js +8 -5
- package/dist/domain-catalog.d.ts +4 -4
- package/dist/domain-catalog.js +4 -4
- package/dist/domain-definition.d.ts +50 -5
- package/dist/domain-definition.js +6 -6
- package/dist/epcis.d.ts +12 -0
- package/dist/epcis.js +12 -0
- package/dist/event-journal.d.ts +31 -1
- package/dist/event-journal.js +27 -1
- package/dist/flow-engine.d.ts +323 -36
- package/dist/flow-engine.js +868 -164
- package/dist/forecast.d.ts +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -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 +31 -11
- package/dist/mes-kernel.d.ts +28 -0
- package/dist/mes-kernel.js +83 -29
- package/dist/mes-profile.d.ts +2 -2
- package/dist/mes-profile.js +11 -11
- package/dist/observed-reducer.d.ts +68 -7
- package/dist/observed-reducer.js +248 -41
- package/dist/task-fold.d.ts +2 -2
- package/dist/task-fold.js +4 -4
- package/dist/vocabulary.d.ts +19 -0
- package/dist/vocabulary.js +66 -0
- package/dist/wms-profile.d.ts +2 -2
- package/dist/wms-profile.js +6 -6
- package/dist/yms-kernel.js +36 -15
- package/dist/yms-profile.d.ts +2 -2
- package/dist/yms-profile.js +6 -6
- package/dist-cjs/index.cjs +1618 -270
- package/package.json +1 -1
package/dist-cjs/index.cjs
CHANGED
|
@@ -25,77 +25,362 @@ __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,
|
|
31
32
|
DOMAIN_SYSTEMS: () => DOMAIN_SYSTEMS,
|
|
32
33
|
EPCIS_CONTEXT: () => EPCIS_CONTEXT,
|
|
34
|
+
EQUIPMENT_LEVEL: () => EQUIPMENT_LEVEL,
|
|
33
35
|
EventJournal: () => EventJournal,
|
|
34
36
|
FlowEngine: () => FlowEngine,
|
|
37
|
+
GUARD_PRAGMA: () => GUARD_PRAGMA,
|
|
35
38
|
ILMD_ATTR: () => ILMD_ATTR,
|
|
39
|
+
LOCATION_SATURATION_NEAR: () => LOCATION_SATURATION_NEAR,
|
|
40
|
+
MATERIAL_PROPERTY: () => MATERIAL_PROPERTY,
|
|
36
41
|
MES_BIZSTEP: () => MES_BIZSTEP,
|
|
37
|
-
|
|
42
|
+
MES_LOCATION_TYPES: () => MES_LOCATION_TYPES,
|
|
38
43
|
MES_PART_GTINS: () => MES_PART_GTINS,
|
|
39
44
|
MES_PRODUCTS: () => MES_PRODUCTS,
|
|
40
45
|
MES_PRODUCT_GTINS: () => MES_PRODUCT_GTINS,
|
|
41
46
|
MES_TYPES: () => MES_TYPES,
|
|
42
47
|
MesKernel: () => MesKernel,
|
|
43
|
-
NODE_SATURATION_NEAR: () => NODE_SATURATION_NEAR,
|
|
44
48
|
OP_EVENT: () => OP_EVENT,
|
|
45
49
|
OP_PARAM: () => OP_PARAM,
|
|
46
50
|
ObservedReducer: () => ObservedReducer,
|
|
51
|
+
PRIORITY_UNSET: () => PRIORITY_UNSET,
|
|
52
|
+
RETIRED_VOCABULARY: () => RETIRED_VOCABULARY,
|
|
47
53
|
StateProjector: () => ObservedReducer,
|
|
48
54
|
TwinHistory: () => TwinHistory,
|
|
49
55
|
TwinObserver: () => TwinObserver,
|
|
50
56
|
TwinRuntime: () => TwinRuntime,
|
|
51
57
|
UTC_OFFSET: () => UTC_OFFSET,
|
|
52
|
-
|
|
58
|
+
VOCABULARY_EXCEPTIONS: () => VOCABULARY_EXCEPTIONS,
|
|
59
|
+
WMS_LOCATION_TYPES: () => WMS_LOCATION_TYPES,
|
|
53
60
|
WMS_TYPES: () => WMS_TYPES,
|
|
54
61
|
WmsKernel: () => WmsKernel,
|
|
55
62
|
YARD_BIZSTEP: () => YARD_BIZSTEP,
|
|
56
|
-
|
|
63
|
+
YMS_LOCATION_TYPES: () => YMS_LOCATION_TYPES,
|
|
57
64
|
YMS_TYPES: () => YMS_TYPES,
|
|
58
65
|
YmsKernel: () => YmsKernel,
|
|
66
|
+
activeShiftAt: () => activeShiftAt,
|
|
67
|
+
activeShiftOf: () => activeShiftOf,
|
|
59
68
|
aggregationEvent: () => aggregationEvent,
|
|
69
|
+
analyzeCapacity: () => analyzeCapacity,
|
|
60
70
|
capabilitiesForType: () => capabilitiesForType,
|
|
71
|
+
classClosure: () => classClosure,
|
|
61
72
|
compareStates: () => compareStates,
|
|
62
73
|
computeOee: () => computeOee,
|
|
63
74
|
constantDuration: () => constantDuration,
|
|
75
|
+
conversionFactorOf: () => conversionFactorOf,
|
|
64
76
|
counterfactualAt: () => counterfactualAt,
|
|
65
77
|
deriveAttentions: () => deriveAttentions,
|
|
78
|
+
dueStatusOf: () => dueStatusOf,
|
|
79
|
+
effectivityAt: () => effectivityAt,
|
|
66
80
|
fefoPolicy: () => fefoPolicy,
|
|
67
81
|
firstFitPolicy: () => firstFitPolicy,
|
|
82
|
+
foldJobResponses: () => foldJobResponses,
|
|
68
83
|
foldTaskRecords: () => foldTaskRecords,
|
|
69
84
|
gdtiUri: () => gdtiUri,
|
|
70
85
|
graiUri: () => graiUri,
|
|
86
|
+
hierarchyOf: () => hierarchyOf,
|
|
87
|
+
inWorkCalendar: () => inWorkCalendar,
|
|
88
|
+
inWorkCalendarAt: () => inWorkCalendarAt,
|
|
71
89
|
ingest: () => ingest,
|
|
90
|
+
isEquipmentLevel: () => isEquipmentLevel,
|
|
91
|
+
isoDurationHours: () => isoDurationHours,
|
|
92
|
+
itemKeyOf: () => itemKeyOf,
|
|
72
93
|
lgtinClass: () => lgtinClass,
|
|
94
|
+
locationStatusOf: () => locationStatusOf,
|
|
73
95
|
mapRecord: () => mapRecord,
|
|
96
|
+
minuteOfDayAt: () => minuteOfDayAt,
|
|
74
97
|
monteCarloForecast: () => monteCarloForecast,
|
|
75
|
-
nodeStatusOf: () => nodeStatusOf,
|
|
76
98
|
objectEvent: () => objectEvent,
|
|
99
|
+
offCalendarAt: () => offCalendarAt,
|
|
100
|
+
offCalendarReasonAt: () => offCalendarReasonAt,
|
|
77
101
|
parseEpc: () => parseEpc,
|
|
78
102
|
parseIsoDuration: () => parseIsoDuration,
|
|
79
103
|
partialFitPolicy: () => partialFitPolicy,
|
|
104
|
+
priorityRank: () => priorityRank,
|
|
105
|
+
quantityIn: () => quantityIn,
|
|
106
|
+
readBoardAssets: () => readBoardAssets,
|
|
107
|
+
readBoardEquipment: () => readBoardEquipment,
|
|
108
|
+
readBoardLocations: () => readBoardLocations,
|
|
80
109
|
replay: () => replay,
|
|
110
|
+
replaySegments: () => replaySegments,
|
|
111
|
+
retiredVocabularyIn: () => retiredVocabularyIn,
|
|
81
112
|
sgtinClass: () => sgtinClass,
|
|
82
113
|
sgtinUri: () => sgtinUri,
|
|
83
114
|
ssccUri: () => ssccUri,
|
|
84
115
|
stateFieldsOf: () => stateFieldsOf,
|
|
116
|
+
subLotIdOf: () => subLotIdOf,
|
|
85
117
|
transactionEvent: () => transactionEvent,
|
|
86
118
|
transformationEvent: () => transformationEvent,
|
|
87
119
|
validateDomainDefinition: () => validateDomainDefinition,
|
|
88
|
-
validateEpcisEvent: () => validateEpcisEvent
|
|
120
|
+
validateEpcisEvent: () => validateEpcisEvent,
|
|
121
|
+
weekdayAt: () => weekdayAt,
|
|
122
|
+
workingTimeOfWeek: () => workingTimeOfWeek
|
|
89
123
|
});
|
|
90
124
|
module.exports = __toCommonJS(index_exports);
|
|
91
125
|
|
|
92
126
|
// src/contract.ts
|
|
93
|
-
var
|
|
94
|
-
function
|
|
127
|
+
var LOCATION_SATURATION_NEAR = 0.9;
|
|
128
|
+
function locationStatusOf(n) {
|
|
95
129
|
const cap = n.capacity;
|
|
96
130
|
if (!(typeof cap === "number" && cap > 0)) return void 0;
|
|
97
131
|
const r = (n.occupancy ?? 0) / cap;
|
|
98
|
-
return r >= 1 ? "full" : r >=
|
|
132
|
+
return r >= 1 ? "full" : r >= LOCATION_SATURATION_NEAR ? "near-full" : "available";
|
|
133
|
+
}
|
|
134
|
+
var EQUIPMENT_LEVEL = [
|
|
135
|
+
"Enterprise",
|
|
136
|
+
"Site",
|
|
137
|
+
"Area",
|
|
138
|
+
"ProcessCell",
|
|
139
|
+
"Unit",
|
|
140
|
+
"ProductionLine",
|
|
141
|
+
"WorkCell",
|
|
142
|
+
"ProductionUnit",
|
|
143
|
+
"StorageZone",
|
|
144
|
+
"StorageUnit",
|
|
145
|
+
"WorkCenter",
|
|
146
|
+
"WorkUnit",
|
|
147
|
+
"EquipmentModule",
|
|
148
|
+
"ControlModule",
|
|
149
|
+
"Other"
|
|
150
|
+
];
|
|
151
|
+
function isEquipmentLevel(v) {
|
|
152
|
+
return typeof v === "string" && EQUIPMENT_LEVEL.includes(v);
|
|
153
|
+
}
|
|
154
|
+
function hierarchyOf(s) {
|
|
155
|
+
const push = (m, k, v) => {
|
|
156
|
+
const cur = m.get(k);
|
|
157
|
+
if (cur) cur.push(v);
|
|
158
|
+
else m.set(k, [v]);
|
|
159
|
+
};
|
|
160
|
+
const parent = /* @__PURE__ */ new Map();
|
|
161
|
+
const kids = /* @__PURE__ */ new Map();
|
|
162
|
+
const known = new Set(s.locations.map((n) => n.id));
|
|
163
|
+
for (const n of s.locations) {
|
|
164
|
+
if (!n.parentId || n.parentId === n.id) continue;
|
|
165
|
+
parent.set(n.id, n.parentId);
|
|
166
|
+
if (known.has(n.parentId)) push(kids, n.parentId, n.id);
|
|
167
|
+
}
|
|
168
|
+
for (const start of known) {
|
|
169
|
+
const seen = [start];
|
|
170
|
+
for (let at = parent.get(start); at !== void 0; at = parent.get(at)) {
|
|
171
|
+
if (seen.includes(at)) throw new Error(`location hierarchy has a cycle: ${[...seen, at].join(" \u2192 ")}`);
|
|
172
|
+
seen.push(at);
|
|
173
|
+
if (!known.has(at)) break;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const homes = /* @__PURE__ */ new Map();
|
|
177
|
+
for (const m of s.equipment ?? []) if (m.homeLocation) push(homes, m.homeLocation, m.id);
|
|
178
|
+
const ancestorsOf = (id) => {
|
|
179
|
+
const out = [];
|
|
180
|
+
for (let at = parent.get(id); at !== void 0; at = parent.get(at)) {
|
|
181
|
+
out.push(at);
|
|
182
|
+
if (!known.has(at)) break;
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
};
|
|
186
|
+
const descendantsOf = (id) => {
|
|
187
|
+
const out = [];
|
|
188
|
+
const stack = [...kids.get(id) ?? []];
|
|
189
|
+
while (stack.length) {
|
|
190
|
+
const cur = stack.pop();
|
|
191
|
+
out.push(cur);
|
|
192
|
+
stack.push(...kids.get(cur) ?? []);
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
};
|
|
196
|
+
const typeOf = new Map(s.locations.map((n) => [n.id, n.type]));
|
|
197
|
+
const levelOf = new Map(s.locations.map((n) => [n.id, n.level]));
|
|
198
|
+
return {
|
|
199
|
+
childrenOf: (id) => [...kids.get(id) ?? []],
|
|
200
|
+
ancestorsOf,
|
|
201
|
+
descendantsOf,
|
|
202
|
+
rollupOf: (id) => ancestorsOf(id).find((a) => !known.has(a)),
|
|
203
|
+
ancestorOfLevel: (id, level) => ancestorsOf(id).find((a) => known.has(a) && levelOf.get(a) === level),
|
|
204
|
+
ancestorOfType: (id, type) => ancestorsOf(id).find((a) => known.has(a) && typeOf.get(a) === type),
|
|
205
|
+
equipmentOf: (id, opts) => {
|
|
206
|
+
const scope = opts?.deep ? [id, ...descendantsOf(id)] : [id];
|
|
207
|
+
return scope.flatMap((n) => homes.get(n) ?? []);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
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
|
+
}
|
|
221
|
+
function classClosure(directIds, defs, at) {
|
|
222
|
+
const byId = new Map((defs ?? []).map((d) => [d.id, d]));
|
|
223
|
+
const inWindow = (d) => !d || effectivityAt(d, at) === void 0;
|
|
224
|
+
const out = /* @__PURE__ */ new Set();
|
|
225
|
+
const stack = [...directIds ?? []];
|
|
226
|
+
while (stack.length) {
|
|
227
|
+
const id = stack.pop();
|
|
228
|
+
if (out.has(id)) continue;
|
|
229
|
+
const def = byId.get(id);
|
|
230
|
+
if (!inWindow(def)) continue;
|
|
231
|
+
out.add(id);
|
|
232
|
+
for (const b of def?.baseIds ?? []) if (!out.has(b)) stack.push(b);
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
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);
|
|
99
384
|
}
|
|
100
385
|
var OP_EVENT = {
|
|
101
386
|
task: "task.status",
|
|
@@ -114,7 +399,7 @@ var CMD = {
|
|
|
114
399
|
orderRelease: "order.release",
|
|
115
400
|
attentionAck: "attention.ack",
|
|
116
401
|
// 주목 신호 확인(OPC UA A&C acknowledge) — args:{id}
|
|
117
|
-
// Operable 코어 — 모든 operable 자원(
|
|
402
|
+
// Operable 코어 — 모든 operable 자원(설비·이동설비) 공통. args:{resourceId}. capability-keyed(무방언, equipment.* 아님).
|
|
118
403
|
resourceHold: "resource.hold",
|
|
119
404
|
// 계획 정지(정비/오프라인) — 배정 스킵
|
|
120
405
|
resourceResume: "resource.resume",
|
|
@@ -126,8 +411,27 @@ var CMD = {
|
|
|
126
411
|
resourceResetMetrics: "resource.reset-metrics",
|
|
127
412
|
// OEE 계측 창 리셋
|
|
128
413
|
resourceAdd: "resource.add"
|
|
129
|
-
// 라이브 자원(
|
|
414
|
+
// 라이브 자원(설비) 추가 — args:{kind, homeLocation, count?}. 런타임 구조 변이(what-if 아닌 실제 act)
|
|
130
415
|
};
|
|
416
|
+
function readBoardLocations(def) {
|
|
417
|
+
const d = def;
|
|
418
|
+
return d.locations ?? d.nodes ?? [];
|
|
419
|
+
}
|
|
420
|
+
function readBoardEquipment(def) {
|
|
421
|
+
const d = def;
|
|
422
|
+
const list = d.equipment ?? d.equipmentList ?? [];
|
|
423
|
+
return list.map((e) => normalizeHomeLocation(e));
|
|
424
|
+
}
|
|
425
|
+
function normalizeHomeLocation(entry) {
|
|
426
|
+
const legacy = entry.homeNode;
|
|
427
|
+
if (legacy === void 0 || entry.homeLocation !== void 0) return entry;
|
|
428
|
+
const { homeNode: _drop, ...rest } = entry;
|
|
429
|
+
return { ...rest, homeLocation: legacy };
|
|
430
|
+
}
|
|
431
|
+
function readBoardAssets(def) {
|
|
432
|
+
const list = def.assets ?? [];
|
|
433
|
+
return list.map((a) => normalizeHomeLocation(a));
|
|
434
|
+
}
|
|
131
435
|
|
|
132
436
|
// src/domain-definition.ts
|
|
133
437
|
var OP_PARAM = {
|
|
@@ -152,19 +456,19 @@ function validateDomainDefinition(def) {
|
|
|
152
456
|
if (!def || typeof def !== "object") return ["domain definition \uC5C6\uC74C/\uAC1D\uCCB4 \uC544\uB2D8"];
|
|
153
457
|
if (typeof def.id !== "string" || !def.id) v.push("id \uB204\uB77D");
|
|
154
458
|
if (typeof def.label !== "string" || !def.label) v.push("label \uB204\uB77D");
|
|
155
|
-
if (!Array.isArray(def.
|
|
459
|
+
if (!Array.isArray(def.locationTypes) || def.locationTypes.length === 0) v.push("locationTypes \uBE44\uC5B4\uC788\uC74C");
|
|
156
460
|
if (!Array.isArray(def.resourceTypes)) v.push("resourceTypes \uBC30\uC5F4 \uC544\uB2D8");
|
|
157
|
-
const
|
|
461
|
+
const locationKeys2 = new Set((def.locationTypes || []).map((n) => n.key));
|
|
158
462
|
const resKeys = new Set((def.resourceTypes || []).map((r) => r.key));
|
|
159
463
|
const matKeys = new Set((def.materials || []).map((m) => m.key));
|
|
160
464
|
const opKeys = new Set((def.operations || []).map((o) => o.key));
|
|
161
465
|
const routeKeys = new Set((def.routes || []).map((r) => r.key));
|
|
162
|
-
for (const [name, arr] of [["
|
|
466
|
+
for (const [name, arr] of [["locationTypes", def.locationTypes], ["resourceTypes", def.resourceTypes], ["materials", def.materials], ["operations", def.operations], ["routes", def.routes], ["recipes", def.recipes]]) {
|
|
163
467
|
for (const d of dupes((arr || []).map((x) => x.key))) v.push(`${name} \uD0A4 \uC911\uBCF5: ${d}`);
|
|
164
468
|
}
|
|
165
469
|
for (const o of def.operations || []) {
|
|
166
470
|
if (!INTENTS.includes(o.intent)) v.push(`operation '${o.key}' intent \uBD80\uC815: ${o.intent}`);
|
|
167
|
-
if (o.
|
|
471
|
+
if (o.locationType && !locationKeys2.has(o.locationType)) v.push(`operation '${o.key}' locationType '${o.locationType}' \uBBF8\uC815\uC758`);
|
|
168
472
|
if (o.resourceType && !resKeys.has(o.resourceType)) v.push(`operation '${o.key}' resourceType '${o.resourceType}' \uBBF8\uC815\uC758`);
|
|
169
473
|
if (o.intent === "dwell" && o.resourceType) v.push(`operation '${o.key}' dwell \uC778\uB370 resourceType \uC9C0\uC815\uB428(\uBB34\uC790\uC6D0\uC774\uC5B4\uC57C)`);
|
|
170
474
|
}
|
|
@@ -196,12 +500,12 @@ function diffBy(predicted, actual, idOf, valOf) {
|
|
|
196
500
|
return out.sort((x, y) => x.id.localeCompare(y.id));
|
|
197
501
|
}
|
|
198
502
|
function compareStates(predicted, actual) {
|
|
199
|
-
const itemLocation = diffBy(predicted.items, actual.items,
|
|
200
|
-
const itemDisposition = diffBy(predicted.items, actual.items,
|
|
201
|
-
const
|
|
503
|
+
const itemLocation = diffBy(predicted.items, actual.items, itemKeyOf, (i) => i.location);
|
|
504
|
+
const itemDisposition = diffBy(predicted.items, actual.items, itemKeyOf, (i) => i.disposition);
|
|
505
|
+
const locationOccupancy = diffBy(predicted.locations, actual.locations, (n) => n.id, (n) => n.occupancy);
|
|
202
506
|
const orderStatus = diffBy(predicted.orders, actual.orders, (o) => o.id, (o) => o.status);
|
|
203
|
-
const hasDrift = itemLocation.length > 0 || itemDisposition.length > 0 ||
|
|
204
|
-
return { hasDrift, itemLocation, itemDisposition,
|
|
507
|
+
const hasDrift = itemLocation.length > 0 || itemDisposition.length > 0 || locationOccupancy.length > 0 || orderStatus.length > 0;
|
|
508
|
+
return { hasDrift, itemLocation, itemDisposition, locationOccupancy, orderStatus };
|
|
205
509
|
}
|
|
206
510
|
|
|
207
511
|
// src/counterfactual.ts
|
|
@@ -324,6 +628,12 @@ var DISP = {
|
|
|
324
628
|
non_sellable: "urn:epcglobal:cbv:disp:non_sellable_other"
|
|
325
629
|
// 불량/scrap
|
|
326
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
|
+
};
|
|
327
637
|
function ssccUri(companyPrefix, serial) {
|
|
328
638
|
return `urn:epc:id:sscc:${companyPrefix}.${String(serial).padStart(10, "0")}`;
|
|
329
639
|
}
|
|
@@ -529,6 +839,12 @@ function validateEpcisEvent(e) {
|
|
|
529
839
|
|
|
530
840
|
// src/observed-reducer.ts
|
|
531
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
|
+
}
|
|
532
848
|
var ObservedReducer = class {
|
|
533
849
|
/** 로케이션 마스터 — 출처를 함께 들고 있다(마스터가 말한 자리 vs 관측으로 알게 된 자리). */
|
|
534
850
|
master = /* @__PURE__ */ new Map();
|
|
@@ -538,34 +854,109 @@ var ObservedReducer = class {
|
|
|
538
854
|
pendingParent = /* @__PURE__ */ new Map();
|
|
539
855
|
// 자식 EPC → 부모(물류단위)
|
|
540
856
|
tasks = /* @__PURE__ */ new Map();
|
|
541
|
-
|
|
857
|
+
equipment = /* @__PURE__ */ new Map();
|
|
542
858
|
persons = /* @__PURE__ */ new Map();
|
|
543
859
|
assets = /* @__PURE__ */ new Map();
|
|
544
860
|
orders = /* @__PURE__ */ new Map();
|
|
545
861
|
revision = 0;
|
|
546
862
|
/** 받은 정정 선언 — 상태에 반영하지 않되 **버리지도 않는다**(소비처가 볼 수 있게). */
|
|
547
863
|
corrections = [];
|
|
864
|
+
/**
|
|
865
|
+
* 자원별 **교대 선언** — 미러가 "지금 근무 중인가" 를 스스로 판정하기 위한 재료.
|
|
866
|
+
*
|
|
867
|
+
* 상태(`EquipmentState`)에 넣지 않는다: 시뮬 스냅샷도 이것을 내보내지 않으므로 넣으면 두 구동이
|
|
868
|
+
* 갈라진다(적합성 하네스가 `mirrorOnly` 로 잡는다). 이것은 **판정의 입력**이고, 나가는 것은 판정뿐이다.
|
|
869
|
+
*/
|
|
870
|
+
shifts = /* @__PURE__ */ new Map();
|
|
871
|
+
/** 시각 해석 기준(보드 선언) — 없으면 UTC. 캘린더의 `HH:MM` 이 어느 기준인지 정한다. */
|
|
872
|
+
utcOffsetMinutes;
|
|
548
873
|
constructor(board) {
|
|
549
|
-
|
|
550
|
-
for (const
|
|
551
|
-
for (const
|
|
552
|
-
for (const
|
|
874
|
+
this.utcOffsetMinutes = board.utcOffsetMinutes;
|
|
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" });
|
|
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 };
|
|
553
944
|
}
|
|
554
945
|
/** 마스터 동기 — 로케이션 추가/변경/제거. */
|
|
555
946
|
applyMaster(u) {
|
|
556
947
|
if (u.op === "remove") {
|
|
557
|
-
this.master.delete(u.
|
|
948
|
+
this.master.delete(u.location.id);
|
|
558
949
|
return;
|
|
559
950
|
}
|
|
560
|
-
const cur = this.master.get(u.
|
|
561
|
-
const capacity = u.
|
|
562
|
-
const parallelism = u.
|
|
563
|
-
this.master.set(u.
|
|
564
|
-
id: u.
|
|
565
|
-
type: u.
|
|
951
|
+
const cur = this.master.get(u.location.id);
|
|
952
|
+
const capacity = u.location.capacity ?? cur?.capacity;
|
|
953
|
+
const parallelism = u.location.parallelism ?? cur?.parallelism;
|
|
954
|
+
this.master.set(u.location.id, {
|
|
955
|
+
id: u.location.id,
|
|
956
|
+
type: u.location.type ?? cur?.type ?? UNKNOWN_TYPE,
|
|
566
957
|
...capacity === void 0 ? {} : { capacity },
|
|
567
958
|
...parallelism === void 0 ? {} : { parallelism },
|
|
568
|
-
...u.
|
|
959
|
+
...u.location.parentId ?? cur?.parentId ? { parentId: u.location.parentId ?? cur?.parentId } : {},
|
|
569
960
|
/* 마스터가 말한 것은 마스터 출처다 — 관측으로 알게 된 것(origin='observed')을 덮어 승격한다. */
|
|
570
961
|
origin: "master"
|
|
571
962
|
});
|
|
@@ -573,7 +964,7 @@ var ObservedReducer = class {
|
|
|
573
964
|
/**
|
|
574
965
|
* 관측된 로케이션을 구조로 승격 — **이벤트가 가르쳐 준 것을 구조에서 지우지 않는다.**
|
|
575
966
|
*
|
|
576
|
-
* 마스터에 없는 로케이션에서 물품이 관측되면, 예전에는 물품의 `location` 에만 남고 `
|
|
967
|
+
* 마스터에 없는 로케이션에서 물품이 관측되면, 예전에는 물품의 `location` 에만 남고 `locations` 에는
|
|
577
968
|
* 나타나지 않았다. 그 결과 그 자리는 스키매틱에 없고, 점유가 집계되지 않고, 병목 주목이 뜰 수
|
|
578
969
|
* 없었다 — **사실은 들어왔는데 구조가 모르는 상태.** 이제 최소 형태로 승격한다:
|
|
579
970
|
* 종류는 모르므로 `unknown`, **용량은 비워 둔다**(발명하지 않는다), 출처는 `observed`.
|
|
@@ -597,6 +988,7 @@ var ObservedReducer = class {
|
|
|
597
988
|
*/
|
|
598
989
|
stale(key, e) {
|
|
599
990
|
const at = Date.parse(String(e.eventTime ?? ""));
|
|
991
|
+
if (Number.isFinite(at) && (this.observedAtMs === void 0 || at > this.observedAtMs)) this.observedAtMs = at;
|
|
600
992
|
if (!Number.isFinite(at)) return false;
|
|
601
993
|
const recorded = Date.parse(String(e.data?.recordTime ?? ""));
|
|
602
994
|
const seen = this.lastAt.get(key);
|
|
@@ -611,6 +1003,14 @@ var ObservedReducer = class {
|
|
|
611
1003
|
}
|
|
612
1004
|
/** 대상별 마지막 반영 시각 — 순서 판정용(대상=EPC·작업·설비·오더 id). */
|
|
613
1005
|
lastAt = /* @__PURE__ */ new Map();
|
|
1006
|
+
/**
|
|
1007
|
+
* **미러의 "지금"** — 지금까지 들은 것 중 가장 늦은 발생 시각.
|
|
1008
|
+
*
|
|
1009
|
+
* 미러는 스스로 시간을 굴리지 않지만, 시각으로만 일어나는 사실(유효 기간 만료)을 판정하려면 기준이
|
|
1010
|
+
* 필요하다. 미러의 정직한 기준은 **"내가 마지막으로 들은 시점"** 이다. 아무것도 못 들었으면
|
|
1011
|
+
* 판정하지 않는다(모르면 단정하지 않는다).
|
|
1012
|
+
*/
|
|
1013
|
+
observedAtMs;
|
|
614
1014
|
/** 이벤트 1건 반영 — eventType 으로 EPCIS vs 운영 델타 분기. */
|
|
615
1015
|
apply(e) {
|
|
616
1016
|
this.revision++;
|
|
@@ -640,27 +1040,42 @@ var ObservedReducer = class {
|
|
|
640
1040
|
startedAtSimMs: d.startedAtSimMs,
|
|
641
1041
|
...d.personnel?.length ? { personnel: d.personnel.slice() } : {},
|
|
642
1042
|
...d.assets?.length ? { assets: d.assets.slice() } : {},
|
|
643
|
-
...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 } : {}
|
|
644
1049
|
});
|
|
645
1050
|
break;
|
|
646
1051
|
}
|
|
647
1052
|
case OP_EVENT.equipment: {
|
|
648
1053
|
const d = e.data;
|
|
649
|
-
if (this.stale(`
|
|
1054
|
+
if (this.stale(`eq:${d.moverId}`, e)) return;
|
|
650
1055
|
this.touchLocation(d.location);
|
|
651
|
-
const known = this.
|
|
652
|
-
|
|
1056
|
+
const known = this.equipment.get(d.moverId);
|
|
1057
|
+
const homeLocation = d.homeLocation ?? known?.homeLocation;
|
|
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 } : {} });
|
|
653
1060
|
break;
|
|
654
1061
|
}
|
|
655
1062
|
case OP_EVENT.person: {
|
|
656
1063
|
const d = e.data;
|
|
657
1064
|
if (this.stale(`person:${d.personId}`, e)) return;
|
|
1065
|
+
const known = this.persons.get(d.personId);
|
|
658
1066
|
this.persons.set(d.personId, {
|
|
659
1067
|
id: d.personId,
|
|
660
|
-
|
|
1068
|
+
personnelClassIds: d.personnelClassIds ?? known?.personnelClassIds,
|
|
661
1069
|
status: d.status,
|
|
662
1070
|
taskId: d.taskId,
|
|
663
|
-
...d.
|
|
1071
|
+
...d.location ?? known?.location ? { location: d.location ?? known?.location } : {},
|
|
1072
|
+
...known?.properties ? { properties: known.properties } : {},
|
|
1073
|
+
...known?.testSpecificationIds ? { testSpecificationIds: known.testSpecificationIds } : {},
|
|
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)
|
|
664
1079
|
});
|
|
665
1080
|
break;
|
|
666
1081
|
}
|
|
@@ -670,11 +1085,12 @@ var ObservedReducer = class {
|
|
|
670
1085
|
const cur = this.assets.get(d.assetId);
|
|
671
1086
|
this.assets.set(d.assetId, {
|
|
672
1087
|
id: d.assetId,
|
|
673
|
-
|
|
1088
|
+
assetClassIds: d.assetClassIds ?? cur?.assetClassIds,
|
|
674
1089
|
location: d.location ?? cur?.location,
|
|
675
1090
|
status: d.status,
|
|
676
1091
|
taskId: d.taskId,
|
|
677
|
-
...d.carrying ? { carrying: d.carrying } : {}
|
|
1092
|
+
...d.carrying ? { carrying: d.carrying } : {},
|
|
1093
|
+
...d.effectiveStart || d.effectiveEnd ? effectiveOf(d) : effectiveOf(cur)
|
|
678
1094
|
});
|
|
679
1095
|
this.touchLocation(d.location);
|
|
680
1096
|
break;
|
|
@@ -690,6 +1106,9 @@ var ObservedReducer = class {
|
|
|
690
1106
|
requested: d.requested,
|
|
691
1107
|
fulfilled: d.fulfilled,
|
|
692
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 } : {},
|
|
693
1112
|
held: d.held
|
|
694
1113
|
});
|
|
695
1114
|
break;
|
|
@@ -742,15 +1161,19 @@ var ObservedReducer = class {
|
|
|
742
1161
|
return;
|
|
743
1162
|
}
|
|
744
1163
|
const loc = ev.readPoint?.id;
|
|
745
|
-
const
|
|
1164
|
+
const all = ev.quantityList ?? [];
|
|
1165
|
+
const q = all[0];
|
|
746
1166
|
this.touchLocation(loc);
|
|
747
1167
|
for (const epc of ev.epcList) {
|
|
748
1168
|
if (envelope && this.stale(`item:${epc}`, envelope)) continue;
|
|
749
|
-
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));
|
|
750
1170
|
}
|
|
751
1171
|
if (!ev.epcList?.length) {
|
|
752
1172
|
for (const qe of ev.quantityList ?? []) {
|
|
753
|
-
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));
|
|
754
1177
|
}
|
|
755
1178
|
}
|
|
756
1179
|
}
|
|
@@ -773,13 +1196,14 @@ var ObservedReducer = class {
|
|
|
773
1196
|
}
|
|
774
1197
|
return void 0;
|
|
775
1198
|
}
|
|
776
|
-
mergeItem(epc, patch, q) {
|
|
777
|
-
const cur = this.items.get(epc);
|
|
1199
|
+
mergeItem(epc, patch, q, all, subLotId) {
|
|
1200
|
+
const cur = this.items.get(subLotId ?? epc);
|
|
778
1201
|
const parsedClass = q?.epcClass ? parseEpc(q.epcClass) : void 0;
|
|
779
1202
|
const parsedSelf = parseEpc(epc);
|
|
780
1203
|
const classUri = q?.epcClass ?? (parsedSelf.instance ? void 0 : epc);
|
|
781
1204
|
return {
|
|
782
1205
|
epc,
|
|
1206
|
+
...subLotId ? { subLotId } : {},
|
|
783
1207
|
gtin: classUri ?? cur?.gtin,
|
|
784
1208
|
gtinKey: parsedClass?.gtinKey ?? parsedSelf.gtinKey ?? cur?.gtinKey,
|
|
785
1209
|
location: patch.location ?? cur?.location ?? "",
|
|
@@ -787,6 +1211,19 @@ var ObservedReducer = class {
|
|
|
787
1211
|
parent: cur?.parent ?? this.pendingParent.get(epc),
|
|
788
1212
|
qty: q?.quantity ?? cur?.qty,
|
|
789
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
|
+
})(),
|
|
790
1227
|
/* 마스터데이터는 생겨날 때 한 번 정해진다 — 뒤 이벤트가 지우지 않게 기존 값을 남긴다. */
|
|
791
1228
|
ilmd: patch.ilmd ?? cur?.ilmd,
|
|
792
1229
|
expiry: this.expiryOf(patch.ilmd) ?? cur?.expiry,
|
|
@@ -803,16 +1240,57 @@ var ObservedReducer = class {
|
|
|
803
1240
|
for (const c of children) this.remove(c);
|
|
804
1241
|
}
|
|
805
1242
|
}
|
|
806
|
-
/** 현재 투영 State —
|
|
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
|
+
}
|
|
807
1285
|
snapshot() {
|
|
808
1286
|
const occ = /* @__PURE__ */ new Map();
|
|
809
1287
|
for (const it of this.items.values()) occ.set(it.location, (occ.get(it.location) ?? 0) + 1);
|
|
810
1288
|
return {
|
|
811
1289
|
revision: this.revision,
|
|
812
1290
|
...this.corrections.length ? { corrections: this.corrections.map((c) => ({ ...c })) } : {},
|
|
813
|
-
|
|
1291
|
+
locations: [...this.master.values()].map((n) => {
|
|
814
1292
|
const occupancy = occ.get(n.id) ?? 0;
|
|
815
|
-
const status =
|
|
1293
|
+
const status = locationStatusOf({ occupancy, capacity: n.capacity });
|
|
816
1294
|
return {
|
|
817
1295
|
id: n.id,
|
|
818
1296
|
type: n.type,
|
|
@@ -827,10 +1305,12 @@ var ObservedReducer = class {
|
|
|
827
1305
|
}),
|
|
828
1306
|
/* 들고 있는 것을 전부 내보낸다 — 축소하면 그 자리에서 정보가 사라진다. */
|
|
829
1307
|
items: [...this.items.values()].map((i) => ({ ...i })),
|
|
830
|
-
|
|
831
|
-
|
|
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) })),
|
|
832
1312
|
tasks: [...this.tasks.values()].map((t) => ({ ...t })),
|
|
833
|
-
|
|
1313
|
+
equipment: [...this.equipment.values()].map((m) => ({ ...m, ...this.effectivityPart(m), ...this.offShiftPart(`eq:${m.id}`) })),
|
|
834
1314
|
orders: [...this.orders.values()].map((o) => ({ ...o }))
|
|
835
1315
|
};
|
|
836
1316
|
}
|
|
@@ -858,8 +1338,8 @@ var EventJournal = class {
|
|
|
858
1338
|
return this.events.slice(revision);
|
|
859
1339
|
}
|
|
860
1340
|
/** sim 시각(ISO eventTime) 이하 이벤트. */
|
|
861
|
-
untilSimTime(
|
|
862
|
-
return this.events.filter((e) => e.eventTime <=
|
|
1341
|
+
untilSimTime(iso2) {
|
|
1342
|
+
return this.events.filter((e) => e.eventTime <= iso2);
|
|
863
1343
|
}
|
|
864
1344
|
};
|
|
865
1345
|
function replay(board, events) {
|
|
@@ -867,6 +1347,16 @@ function replay(board, events) {
|
|
|
867
1347
|
for (const e of events) proj.apply(e);
|
|
868
1348
|
return proj.snapshot();
|
|
869
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
|
+
}
|
|
870
1360
|
|
|
871
1361
|
// src/wms-profile.ts
|
|
872
1362
|
var BIZSTEP = {
|
|
@@ -882,10 +1372,10 @@ var BTT = {
|
|
|
882
1372
|
po: "urn:epcglobal:cbv:btt:po",
|
|
883
1373
|
so: "urn:epcglobal:cbv:btt:so"
|
|
884
1374
|
};
|
|
885
|
-
var
|
|
1375
|
+
var WMS_LOCATION_TYPES = ["dock", "storage", "staging", "dock-ship"];
|
|
886
1376
|
var WMS_TYPES = [
|
|
887
|
-
...
|
|
888
|
-
{ key: "forklift", role: "
|
|
1377
|
+
...WMS_LOCATION_TYPES.map((k) => ({ key: k, role: "location", label: `twin.type.${k}`, standardClass: { epcis: "bizLocation" }, identity: { scheme: "gs1:SGLN" }, capabilities: ["storable"] })),
|
|
1378
|
+
{ key: "forklift", role: "equipment", label: "twin.type.forklift", standardClass: { epcis: "object", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["mobile", "operable"] }
|
|
889
1379
|
];
|
|
890
1380
|
|
|
891
1381
|
// src/capability.ts
|
|
@@ -908,7 +1398,7 @@ var CAPABILITIES = {
|
|
|
908
1398
|
mobile: {
|
|
909
1399
|
key: "mobile",
|
|
910
1400
|
label: "\uC774\uB3D9",
|
|
911
|
-
semantics: "\uC790\uC6D0 \uC790\uC2E0\uC774 \
|
|
1401
|
+
semantics: "\uC790\uC6D0 \uC790\uC2E0\uC774 \uC790\uB9AC \uAC04 \uC774\uB3D9. Transferable(\uC544\uC774\uD15C \uC774\uB3D9)\uACFC \uB2E4\uB984. (\uC52C \uAE30\uC81C: CarrierLine)",
|
|
912
1402
|
stateFields: ["location", "motion"],
|
|
913
1403
|
models: ["Motion"],
|
|
914
1404
|
results: ["moved", "motionTick"]
|
|
@@ -952,10 +1442,10 @@ var BTT_DELIVERY = "urn:epcglobal:cbv:btt:deliv";
|
|
|
952
1442
|
function graiUri(companyPrefix, assetType, serial) {
|
|
953
1443
|
return `urn:epc:id:grai:${companyPrefix}.${assetType}.${String(serial).padStart(6, "0")}`;
|
|
954
1444
|
}
|
|
955
|
-
var
|
|
1445
|
+
var YMS_LOCATION_TYPES = ["gate", "yard-slot", "dock-door", "staging"];
|
|
956
1446
|
var YMS_TYPES = [
|
|
957
|
-
...
|
|
958
|
-
{ key: "hostler", role: "
|
|
1447
|
+
...YMS_LOCATION_TYPES.map((k) => ({ key: k, role: "location", label: `twin.type.${k}`, standardClass: { epcis: "bizLocation" }, identity: { scheme: "gs1:SGLN" }, capabilities: ["storable"] })),
|
|
1448
|
+
{ key: "hostler", role: "equipment", label: "twin.type.hostler", standardClass: { epcis: "object", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["mobile", "operable"] }
|
|
959
1449
|
];
|
|
960
1450
|
|
|
961
1451
|
// src/mes-profile.ts
|
|
@@ -971,8 +1461,8 @@ var BTT_PRODORDER = "urn:epcglobal:cbv:btt:prodorder";
|
|
|
971
1461
|
function sgtinUri(companyPrefix, itemRef, serial) {
|
|
972
1462
|
return `urn:epc:id:sgtin:${companyPrefix}.${itemRef}.${serial}`;
|
|
973
1463
|
}
|
|
974
|
-
var
|
|
975
|
-
var
|
|
1464
|
+
var MES_LOCATION_TYPES = ["raw-store", "cut-station", "weld-station", "paint-booth", "assembly-line", "fg-store"];
|
|
1465
|
+
var MES_LOCATION_CLS = {
|
|
976
1466
|
"raw-store": { epcis: "bizLocation" },
|
|
977
1467
|
"cut-station": { isa95: "WorkCenter", epcis: "bizLocation" },
|
|
978
1468
|
"weld-station": { isa95: "WorkCenter", epcis: "bizLocation" },
|
|
@@ -981,20 +1471,20 @@ var MES_NODE_CLS = {
|
|
|
981
1471
|
"fg-store": { epcis: "bizLocation" }
|
|
982
1472
|
};
|
|
983
1473
|
var MES_TYPES = [
|
|
984
|
-
...
|
|
985
|
-
{ key: "cutter", role: "
|
|
986
|
-
{ key: "welder", role: "
|
|
987
|
-
{ key: "painter", role: "
|
|
988
|
-
{ key: "assembler", role: "
|
|
1474
|
+
...MES_LOCATION_TYPES.map((k) => ({ key: k, role: "location", label: `twin.type.${k}`, standardClass: MES_LOCATION_CLS[k] ?? {}, identity: { scheme: "gs1:SGLN" }, capabilities: ["storable"] })),
|
|
1475
|
+
{ key: "cutter", role: "equipment", label: "twin.type.cutter", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] },
|
|
1476
|
+
{ key: "welder", role: "equipment", label: "twin.type.welder", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] },
|
|
1477
|
+
{ key: "painter", role: "equipment", label: "twin.type.painter", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] },
|
|
1478
|
+
{ key: "assembler", role: "equipment", label: "twin.type.assembler", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] }
|
|
989
1479
|
];
|
|
990
1480
|
|
|
991
1481
|
// src/domain-catalog.ts
|
|
992
|
-
var
|
|
1482
|
+
var locationKeys = (types) => types.filter((t) => t.role === "location").map((t) => t.key);
|
|
993
1483
|
var DOMAIN_CATALOG = {
|
|
994
1484
|
// label 은 언어 중립 i18n 키(twin.system.<code>) — 사람 언어는 표현계층이 렌더(L2).
|
|
995
|
-
wms: { system: "wms", label: "twin.system.wms", types: WMS_TYPES,
|
|
996
|
-
yms: { system: "yms", label: "twin.system.yms", types: YMS_TYPES,
|
|
997
|
-
mes: { system: "mes", label: "twin.system.mes", types: MES_TYPES,
|
|
1485
|
+
wms: { system: "wms", label: "twin.system.wms", types: WMS_TYPES, locationTypes: locationKeys(WMS_TYPES) },
|
|
1486
|
+
yms: { system: "yms", label: "twin.system.yms", types: YMS_TYPES, locationTypes: locationKeys(YMS_TYPES) },
|
|
1487
|
+
mes: { system: "mes", label: "twin.system.mes", types: MES_TYPES, locationTypes: locationKeys(MES_TYPES) }
|
|
998
1488
|
};
|
|
999
1489
|
var DOMAIN_SYSTEMS = ["wms", "yms", "mes"];
|
|
1000
1490
|
function capabilitiesForType(system, typeKey) {
|
|
@@ -1083,8 +1573,8 @@ function foldTaskRecords(rows) {
|
|
|
1083
1573
|
if (f.resource === void 0 && d.resourceRef) f.resource = d.resourceRef;
|
|
1084
1574
|
if (f.kind === void 0 && d.kind) f.kind = d.kind;
|
|
1085
1575
|
if (f.order === void 0 && d.orderId) f.order = d.orderId;
|
|
1086
|
-
const
|
|
1087
|
-
if (
|
|
1576
|
+
const loc = d.toNode ?? d.fromNode;
|
|
1577
|
+
if (loc) f.location = loc;
|
|
1088
1578
|
if (d.status === "created") rec.createdMs = at;
|
|
1089
1579
|
else if (d.status === "in-progress") rec.startedMs = rec.startedMs != null ? Math.min(rec.startedMs, at) : at;
|
|
1090
1580
|
else if (d.status === "completed") rec.completedMs = rec.completedMs != null ? Math.max(rec.completedMs, at) : at;
|
|
@@ -1092,6 +1582,46 @@ function foldTaskRecords(rows) {
|
|
|
1092
1582
|
return { records: [...byTask.values()], rowsRead, latestMs };
|
|
1093
1583
|
}
|
|
1094
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
|
+
|
|
1095
1625
|
// src/face2-adapter.ts
|
|
1096
1626
|
function get(obj, path) {
|
|
1097
1627
|
return path.split(".").reduce((o, k) => o == null ? o : o[k], obj);
|
|
@@ -1192,8 +1722,108 @@ var TwinRuntime = class {
|
|
|
1192
1722
|
}
|
|
1193
1723
|
};
|
|
1194
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
|
+
|
|
1195
1819
|
// src/flow-engine.ts
|
|
1196
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
|
+
}
|
|
1197
1827
|
function mulberry32(seed) {
|
|
1198
1828
|
let a = seed >>> 0;
|
|
1199
1829
|
const fn = () => {
|
|
@@ -1207,16 +1837,16 @@ function mulberry32(seed) {
|
|
|
1207
1837
|
} });
|
|
1208
1838
|
return fn;
|
|
1209
1839
|
}
|
|
1210
|
-
function deriveAttentions(view, acked) {
|
|
1840
|
+
function deriveAttentions(view, acked, nowIso) {
|
|
1211
1841
|
const out = [];
|
|
1212
|
-
for (const m of view.
|
|
1842
|
+
for (const m of view.equipment) {
|
|
1213
1843
|
if (m.status === "down") {
|
|
1214
1844
|
out.push({
|
|
1215
1845
|
id: `breakdown:${m.id}`,
|
|
1216
1846
|
kind: "breakdown",
|
|
1217
1847
|
severity: "critical",
|
|
1218
|
-
anchor: { moverId: m.id,
|
|
1219
|
-
params: { moverId: m.id, ...m.location ? {
|
|
1848
|
+
anchor: { moverId: m.id, locationId: m.location },
|
|
1849
|
+
params: { moverId: m.id, ...m.location ? { locationId: m.location } : {} },
|
|
1220
1850
|
recommendedActions: [
|
|
1221
1851
|
{ code: "act.repair", command: CMD.resourceRepair, args: { resourceId: m.id } },
|
|
1222
1852
|
{ code: "act.hold-until-repair", command: CMD.resourceHold, args: { resourceId: m.id } }
|
|
@@ -1225,7 +1855,7 @@ function deriveAttentions(view, acked) {
|
|
|
1225
1855
|
});
|
|
1226
1856
|
}
|
|
1227
1857
|
}
|
|
1228
|
-
for (const n of view.
|
|
1858
|
+
for (const n of view.locations) {
|
|
1229
1859
|
if ((n.capacity ?? 0) > 0) {
|
|
1230
1860
|
const r = (n.occupancy ?? 0) / n.capacity;
|
|
1231
1861
|
if (r >= 0.9) {
|
|
@@ -1234,15 +1864,39 @@ function deriveAttentions(view, acked) {
|
|
|
1234
1864
|
id: `bottleneck:${n.id}`,
|
|
1235
1865
|
kind: "bottleneck",
|
|
1236
1866
|
severity: saturated ? "high" : "medium",
|
|
1237
|
-
anchor: {
|
|
1238
|
-
params: {
|
|
1867
|
+
anchor: { locationId: n.id },
|
|
1868
|
+
params: { locationId: n.id, occupancy: n.occupancy ?? 0, capacity: n.capacity ?? 0, ratioPct: Math.round(r * 100), saturated: saturated ? 1 : 0 },
|
|
1239
1869
|
recommendedActions: [{ code: "advice.add-resource" }, { code: "advice.downstream-priority" }]
|
|
1240
1870
|
// 권고만(command 없음) — 표현계층이 advice.* 코드로 렌더
|
|
1241
1871
|
});
|
|
1242
1872
|
}
|
|
1243
1873
|
}
|
|
1244
1874
|
}
|
|
1245
|
-
|
|
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
|
+
}
|
|
1899
|
+
for (const m of view.equipment) {
|
|
1246
1900
|
const total = (m.goodCount ?? 0) + (m.scrapCount ?? 0);
|
|
1247
1901
|
if (total >= 10) {
|
|
1248
1902
|
const rate = (m.scrapCount ?? 0) / total;
|
|
@@ -1250,7 +1904,7 @@ function deriveAttentions(view, acked) {
|
|
|
1250
1904
|
id: `scrap:${m.id}`,
|
|
1251
1905
|
kind: "scrap-high",
|
|
1252
1906
|
severity: rate >= 0.3 ? "high" : "medium",
|
|
1253
|
-
anchor: { moverId: m.id,
|
|
1907
|
+
anchor: { moverId: m.id, locationId: m.location },
|
|
1254
1908
|
params: { moverId: m.id, goodCount: m.goodCount ?? 0, scrapCount: m.scrapCount ?? 0, ratePct: Math.round(rate * 100) },
|
|
1255
1909
|
recommendedActions: [
|
|
1256
1910
|
{ code: "act.hold-for-inspection", command: CMD.resourceHold, args: { resourceId: m.id } },
|
|
@@ -1271,6 +1925,21 @@ function deriveAttentions(view, acked) {
|
|
|
1271
1925
|
suggestedAction: { code: "act.resume-order", command: CMD.orderResume, args: { orderId: o.id } }
|
|
1272
1926
|
});
|
|
1273
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
|
+
}
|
|
1274
1943
|
if (acked) {
|
|
1275
1944
|
for (const a of out) if (acked.has(a.id)) a.state = "acknowledged";
|
|
1276
1945
|
}
|
|
@@ -1298,9 +1967,16 @@ function computeOee(c, nowMs) {
|
|
|
1298
1967
|
}
|
|
1299
1968
|
var FlowEngine = class {
|
|
1300
1969
|
tenantId;
|
|
1301
|
-
|
|
1970
|
+
locations = /* @__PURE__ */ new Map();
|
|
1302
1971
|
items = /* @__PURE__ */ new Map();
|
|
1303
|
-
|
|
1972
|
+
equipment = /* @__PURE__ */ new Map();
|
|
1973
|
+
/** 등급 정의(표준 `<X>Class`) — 상속·유효기간 판정의 재료. 선언 안 하면 비어 있고, 소속 그대로 판정한다. */
|
|
1974
|
+
classDefs = {};
|
|
1975
|
+
/**
|
|
1976
|
+
* 품목 정의 색인(표준 `MaterialDefinition`) — 단위 환산의 **유일한 근거**.
|
|
1977
|
+
* 비어 있으면 환산하지 않는다(계수를 모르는데 값을 만들지 않는다).
|
|
1978
|
+
*/
|
|
1979
|
+
materialDefs = /* @__PURE__ */ new Map();
|
|
1304
1980
|
/** 사람 — 선언하지 않으면 빈 맵(인원 제약 없는 트윈, 기존 거동). */
|
|
1305
1981
|
persons = /* @__PURE__ */ new Map();
|
|
1306
1982
|
/** 물리 자산 — 선언하지 않으면 빈 맵(자산 제약 없는 트윈, 기존 거동). */
|
|
@@ -1347,88 +2023,191 @@ var FlowEngine = class {
|
|
|
1347
2023
|
// ── TwinKernel (mechanics, 도메인 무관) ───────────────────────────────────
|
|
1348
2024
|
loadBoard(def) {
|
|
1349
2025
|
this.boardDef = def;
|
|
1350
|
-
for (const n of def
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
for (const
|
|
1354
|
-
|
|
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 });
|
|
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) });
|
|
2031
|
+
for (const m of readBoardEquipment(def)) {
|
|
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) };
|
|
1355
2033
|
if (m.mtbfMs !== void 0) {
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
2034
|
+
eq.mtbfMs = m.mtbfMs;
|
|
2035
|
+
eq.mttrMs = m.mttrMs;
|
|
2036
|
+
eq.nextFailureMs = this.sampleExp(m.mtbfMs);
|
|
1359
2037
|
}
|
|
1360
|
-
this.
|
|
2038
|
+
this.equipment.set(m.id, eq);
|
|
1361
2039
|
}
|
|
1362
2040
|
}
|
|
1363
2041
|
/**
|
|
1364
|
-
* what-if 구성 변주 — fork(또는 실행 중) 엔진에
|
|
2042
|
+
* what-if 구성 변주 — fork(또는 실행 중) 엔진에 설비 추가. loadBoard 설비 삽입과 동일 규약.
|
|
1365
2043
|
* 기본은 mtbf 미지정(고장 없는 신뢰 자원) → sampleExp(rng) 무소비라 baseline fork 와 깨끗이 비교 가능.
|
|
1366
2044
|
*/
|
|
1367
|
-
|
|
1368
|
-
if (this.
|
|
1369
|
-
const
|
|
2045
|
+
addEquipment(m) {
|
|
2046
|
+
if (this.equipment.has(m.id)) return;
|
|
2047
|
+
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 };
|
|
1370
2048
|
if (m.mtbfMs !== void 0) {
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
2049
|
+
eq.mtbfMs = m.mtbfMs;
|
|
2050
|
+
eq.mttrMs = m.mttrMs;
|
|
2051
|
+
eq.nextFailureMs = this.sampleExp(m.mtbfMs);
|
|
1374
2052
|
}
|
|
1375
|
-
this.
|
|
2053
|
+
this.equipment.set(m.id, eq);
|
|
1376
2054
|
}
|
|
1377
2055
|
/**
|
|
1378
|
-
* 관측 상태 주입(라이브 예측용, kernel-unification P1) — 외부 관측 스냅샷(
|
|
2056
|
+
* 관측 상태 주입(라이브 예측용, kernel-unification P1) — 외부 관측 스냅샷(재고·설비·자리)과
|
|
1379
2057
|
* 저널 오더(원값+라인)로 이 커널의 맵을 채운다. tick 으로 만든 게 아니라 "현재 관측된 현실"을 심어
|
|
1380
2058
|
* 이후 fork/tick 으로 예측한다. 라이브 런타임은 여전히 projector 미러 — 이 커널은 예측용 임시본.
|
|
1381
2059
|
* 오더는 남은 데맨드(라인별 requested-fulfilled)를 'created' 로 복원(현재 재고에서 재계획).
|
|
1382
2060
|
* 진행 중 개별 task 의 내부 상태는 관측만으론 복원 불가 → 재계획에 맡김(정직한 한계).
|
|
1383
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
|
+
}
|
|
1384
2077
|
hydrateObserved(snap, orders = []) {
|
|
1385
|
-
for (const n of snap.
|
|
1386
|
-
this.
|
|
2078
|
+
for (const n of snap.locations) {
|
|
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 });
|
|
1387
2080
|
}
|
|
1388
2081
|
this.items.clear();
|
|
1389
2082
|
for (const it of snap.items) {
|
|
1390
|
-
this.items.set(it
|
|
2083
|
+
this.items.set(itemKeyOf(it), {
|
|
1391
2084
|
/* 품번 키·로트는 식별자에서 파생되므로 심지 않는다(스냅샷이 다시 낸다 — 두 벌을 두면 어긋난다). */
|
|
1392
2085
|
epc: it.epc,
|
|
1393
2086
|
location: it.location,
|
|
1394
2087
|
disposition: it.disposition ?? DISP.sellable,
|
|
2088
|
+
...it.subLotId ? { subLotId: it.subLotId } : {},
|
|
2089
|
+
...it.definitionId ? { definitionId: it.definitionId } : {},
|
|
1395
2090
|
gtin: it.gtin,
|
|
1396
2091
|
qty: it.qty ?? 1,
|
|
1397
2092
|
uom: it.uom,
|
|
2093
|
+
...it.quantities?.length ? { quantities: it.quantities } : {},
|
|
1398
2094
|
parent: it.parent,
|
|
1399
2095
|
carriedBy: it.carriedBy,
|
|
1400
2096
|
expiry: it.expiry,
|
|
1401
2097
|
ilmd: it.ilmd
|
|
1402
2098
|
});
|
|
1403
2099
|
}
|
|
1404
|
-
for (const m of snap.
|
|
2100
|
+
for (const m of snap.equipment) {
|
|
1405
2101
|
const oee = m.oee;
|
|
1406
|
-
this.
|
|
2102
|
+
const prev = this.equipment.get(m.id);
|
|
2103
|
+
this.equipment.set(m.id, {
|
|
2104
|
+
...prev ?? {},
|
|
1407
2105
|
id: m.id,
|
|
1408
2106
|
kind: m.kind,
|
|
1409
2107
|
location: m.location ?? "",
|
|
2108
|
+
...m.homeLocation ? { homeLocation: m.homeLocation } : {},
|
|
1410
2109
|
status: m.status ?? "idle",
|
|
1411
2110
|
taskId: null,
|
|
1412
2111
|
runMs: oee?.runMs ?? 0,
|
|
1413
2112
|
setupMs: oee?.setupMs ?? 0,
|
|
1414
2113
|
downMs: oee?.downMs ?? 0,
|
|
1415
2114
|
goodCount: oee?.goodCount ?? 0,
|
|
1416
|
-
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)
|
|
1417
2123
|
});
|
|
1418
2124
|
}
|
|
1419
2125
|
for (const p of snap.persons ?? []) {
|
|
1420
|
-
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
|
+
});
|
|
1421
2138
|
}
|
|
1422
2139
|
for (const a of snap.assets ?? []) {
|
|
1423
|
-
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
|
+
});
|
|
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
|
+
});
|
|
1424
2190
|
}
|
|
2191
|
+
const seededOrderIds = new Set(this.orders.keys());
|
|
2192
|
+
let orphaned = 0;
|
|
1425
2193
|
for (const t of snap.tasks ?? []) {
|
|
1426
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
|
+
}
|
|
1427
2204
|
const known = typeof t.remainingMs === "number" && Number.isFinite(t.remainingMs);
|
|
1428
2205
|
this.tasks.set(t.id, {
|
|
1429
2206
|
id: t.id,
|
|
1430
2207
|
kind: t.kind,
|
|
1431
2208
|
status: known && t.status === "in-progress" ? "in-progress" : "created",
|
|
2209
|
+
/* 이미 일어난 자재 이동은 **씨앗에도 남는다** — 잃으면 실적이 재기동마다 지워진다. */
|
|
2210
|
+
...t.materialActual?.length ? { materialActual: t.materialActual.map((r) => ({ ...r })) } : {},
|
|
1432
2211
|
itemEpc: t.itemRefs?.[0] ?? "",
|
|
1433
2212
|
fromNode: t.fromNode ?? "",
|
|
1434
2213
|
toNode: t.toNode ?? "",
|
|
@@ -1440,7 +2219,7 @@ var FlowEngine = class {
|
|
|
1440
2219
|
intent: t.intent
|
|
1441
2220
|
});
|
|
1442
2221
|
if (known && t.status === "in-progress" && t.resourceRef) {
|
|
1443
|
-
const mv = this.
|
|
2222
|
+
const mv = this.equipment.get(t.resourceRef);
|
|
1444
2223
|
if (mv) {
|
|
1445
2224
|
mv.status = "busy";
|
|
1446
2225
|
mv.taskId = t.id;
|
|
@@ -1458,17 +2237,15 @@ var FlowEngine = class {
|
|
|
1458
2237
|
}
|
|
1459
2238
|
}
|
|
1460
2239
|
}
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
if (remaining <= 0) continue;
|
|
1466
|
-
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
|
+
);
|
|
1467
2244
|
}
|
|
1468
2245
|
}
|
|
1469
|
-
/** what-if 구성 변주 —
|
|
1470
|
-
|
|
1471
|
-
const n = this.
|
|
2246
|
+
/** what-if 구성 변주 — 자리 용량 변경(fork 대상). 존재하면 true. */
|
|
2247
|
+
setLocationCapacity(locationId, capacity) {
|
|
2248
|
+
const n = this.locations.get(locationId);
|
|
1472
2249
|
if (!n) return false;
|
|
1473
2250
|
n.capacity = Math.max(0, capacity);
|
|
1474
2251
|
return true;
|
|
@@ -1494,6 +2271,15 @@ var FlowEngine = class {
|
|
|
1494
2271
|
* State 델타를 유발한다(command → 행위 → 관측 폐루프).
|
|
1495
2272
|
*/
|
|
1496
2273
|
_acked = /* @__PURE__ */ new Set();
|
|
2274
|
+
/**
|
|
2275
|
+
* 완료 시점에 **주체가 사라져 접은 작업 수** — 조용한 누락이 되지 않게 센다.
|
|
2276
|
+
*
|
|
2277
|
+
* 물품이 포장·출하·소비로 없어지는 것은 정상이지만, 그 물품을 향한 작업이 남아 있었다는 것은
|
|
2278
|
+
* 상류에 어긋남이 있다는 신호다. 0 이 아니면 그 사실을 소비처가 볼 수 있어야 한다.
|
|
2279
|
+
*/
|
|
2280
|
+
abandonedTasks = 0;
|
|
2281
|
+
/** 주목 신호가 **처음 성립한 시각**(id → ISO). 조건이 사라지면 지운다 — 재발은 새 시작이다. */
|
|
2282
|
+
_attentionSince = /* @__PURE__ */ new Map();
|
|
1497
2283
|
// 확인(ack)된 주목 신호 id — 조건 지속돼도 acknowledged 로 표시(재발 시 재활성)
|
|
1498
2284
|
dispatch(cmd) {
|
|
1499
2285
|
const ok = () => ({ commandId: cmd.commandId, accepted: true });
|
|
@@ -1513,39 +2299,39 @@ var FlowEngine = class {
|
|
|
1513
2299
|
if (id) this._acked.add(id);
|
|
1514
2300
|
return ok();
|
|
1515
2301
|
}
|
|
1516
|
-
// Operable 코어 — 자원(
|
|
2302
|
+
// Operable 코어 — 자원(설비·설비) 제어. capability-keyed(resourceId), 모든 operable 자원 공통.
|
|
1517
2303
|
case CMD.resourceHold:
|
|
1518
2304
|
case CMD.resourceResume: {
|
|
1519
|
-
const m = this.
|
|
2305
|
+
const m = this.equipment.get(cmd.args?.resourceId ?? "");
|
|
1520
2306
|
if (!m) return fail("resource-not-found");
|
|
1521
2307
|
m.held = cmd.type === CMD.resourceHold;
|
|
1522
|
-
this.
|
|
2308
|
+
this.emitEquipment(m);
|
|
1523
2309
|
return ok();
|
|
1524
2310
|
}
|
|
1525
2311
|
case CMD.resourceDown: {
|
|
1526
2312
|
const a = cmd.args;
|
|
1527
|
-
const m = this.
|
|
2313
|
+
const m = this.equipment.get(a?.resourceId ?? "");
|
|
1528
2314
|
if (!m) return fail("resource-not-found");
|
|
1529
2315
|
if (m.status !== "down") {
|
|
1530
2316
|
m.status = "down";
|
|
1531
2317
|
m.repairUntilMs = this.clockMs + (Number(a?.durationMs) || m.mttrMs || 6e4);
|
|
1532
|
-
this.
|
|
2318
|
+
this.emitEquipment(m);
|
|
1533
2319
|
}
|
|
1534
2320
|
return ok();
|
|
1535
2321
|
}
|
|
1536
2322
|
case CMD.resourceRepair: {
|
|
1537
|
-
const m = this.
|
|
2323
|
+
const m = this.equipment.get(cmd.args?.resourceId ?? "");
|
|
1538
2324
|
if (!m) return fail("resource-not-found");
|
|
1539
2325
|
if (m.status === "down") {
|
|
1540
2326
|
m.status = m.taskId ? "busy" : "idle";
|
|
1541
2327
|
m.repairUntilMs = void 0;
|
|
1542
2328
|
if (m.mtbfMs !== void 0) m.nextFailureMs = this.clockMs + this.sampleExp(m.mtbfMs);
|
|
1543
|
-
this.
|
|
2329
|
+
this.emitEquipment(m);
|
|
1544
2330
|
}
|
|
1545
2331
|
return ok();
|
|
1546
2332
|
}
|
|
1547
2333
|
case CMD.resourceResetMetrics: {
|
|
1548
|
-
const m = this.
|
|
2334
|
+
const m = this.equipment.get(cmd.args?.resourceId ?? "");
|
|
1549
2335
|
if (!m) return fail("resource-not-found");
|
|
1550
2336
|
m.runMs = 0;
|
|
1551
2337
|
m.setupMs = 0;
|
|
@@ -1554,21 +2340,21 @@ var FlowEngine = class {
|
|
|
1554
2340
|
m.scrapCount = 0;
|
|
1555
2341
|
m.holdMs = 0;
|
|
1556
2342
|
m.metricsSinceMs = this.clockMs;
|
|
1557
|
-
this.
|
|
2343
|
+
this.emitEquipment(m);
|
|
1558
2344
|
return ok();
|
|
1559
2345
|
}
|
|
1560
2346
|
case CMD.resourceAdd: {
|
|
1561
2347
|
const a = cmd.args;
|
|
1562
2348
|
if (!a?.kind) return fail("kind-required");
|
|
1563
|
-
if (!a?.
|
|
2349
|
+
if (!a?.homeLocation || !this.locations.has(a.homeLocation)) return fail("home-location-not-found", { homeLocation: a?.homeLocation ?? "" });
|
|
1564
2350
|
const count = Math.max(1, Math.min(50, Number(a.count) || 1));
|
|
1565
|
-
let seq = this.
|
|
2351
|
+
let seq = this.equipment.size;
|
|
1566
2352
|
for (let i = 0; i < count; i++) {
|
|
1567
2353
|
let id = `${a.kind}-${++seq}`;
|
|
1568
|
-
while (this.
|
|
1569
|
-
this.
|
|
1570
|
-
const m = this.
|
|
1571
|
-
if (m) this.
|
|
2354
|
+
while (this.equipment.has(id)) id = `${a.kind}-${++seq}`;
|
|
2355
|
+
this.addEquipment({ id, kind: a.kind, homeLocation: a.homeLocation });
|
|
2356
|
+
const m = this.equipment.get(id);
|
|
2357
|
+
if (m) this.emitEquipment(m);
|
|
1572
2358
|
}
|
|
1573
2359
|
return ok();
|
|
1574
2360
|
}
|
|
@@ -1615,28 +2401,42 @@ var FlowEngine = class {
|
|
|
1615
2401
|
return {
|
|
1616
2402
|
revision: this.revision,
|
|
1617
2403
|
simClockMs: this.clockMs,
|
|
2404
|
+
nowTime: this.now(),
|
|
2405
|
+
// 트윈의 "지금" — 관측 모드면 마지막으로 들은 시각(§nowMs)
|
|
1618
2406
|
/* 출처 표시 — 보드(마스터)에서 온 자리다. 미러는 관측으로 알게 된 자리를 'observed' 로 구별하는데,
|
|
1619
2407
|
* 시뮬이 아무 표시도 안 하면 소비처가 두 스냅샷을 같은 규칙으로 읽지 못한다. */
|
|
1620
|
-
|
|
2408
|
+
locations: [...this.locations.values()].map((n) => {
|
|
1621
2409
|
const { status, ...rest } = n;
|
|
1622
|
-
const derived =
|
|
2410
|
+
const derived = locationStatusOf(n);
|
|
1623
2411
|
return { ...rest, ...derived ? { status: derived } : {}, origin: "master" };
|
|
1624
2412
|
}),
|
|
1625
2413
|
items: [...this.items.values()].map((i) => this.itemState(i)),
|
|
1626
|
-
|
|
1627
|
-
const s = { id: m.id, kind: m.kind, location: m.location, status: m.status, taskId: m.taskId ?? void 0, oee: this.oeeOf(m), held: m.held, origin: "master", ...this.offShift(m) ? { offShift: true } : {} };
|
|
2414
|
+
equipment: [...this.equipment.values()].map((m) => {
|
|
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) } : {} };
|
|
1628
2416
|
const t = m.taskId ? this.tasks.get(m.taskId) : void 0;
|
|
1629
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 };
|
|
1630
2418
|
return s;
|
|
1631
2419
|
}),
|
|
1632
2420
|
assets: [...this.assets.values()].map((a) => {
|
|
1633
|
-
const st = { id: a.id,
|
|
2421
|
+
const st = { id: a.id, assetClassIds: a.assetClassIds, location: a.location, status: a.status, taskId: a.taskId ?? void 0 };
|
|
1634
2422
|
if (a.carrying) st.carrying = a.carrying;
|
|
2423
|
+
if (a.properties) st.properties = a.properties;
|
|
2424
|
+
if (a.testSpecificationIds) st.testSpecificationIds = a.testSpecificationIds;
|
|
2425
|
+
Object.assign(st, this.effectivePart(a));
|
|
1635
2426
|
return st;
|
|
1636
2427
|
}),
|
|
1637
2428
|
persons: [...this.persons.values()].map((p) => {
|
|
1638
|
-
const st = { id: p.id,
|
|
1639
|
-
if (
|
|
2429
|
+
const st = { id: p.id, personnelClassIds: p.personnelClassIds, status: p.status, taskId: p.taskId ?? void 0 };
|
|
2430
|
+
if (p.location) st.location = p.location;
|
|
2431
|
+
if (p.properties) st.properties = p.properties;
|
|
2432
|
+
if (p.testSpecificationIds) st.testSpecificationIds = p.testSpecificationIds;
|
|
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));
|
|
1640
2440
|
return st;
|
|
1641
2441
|
}),
|
|
1642
2442
|
/* 스냅샷이 **델타보다 가난하면 안 된다** — 예전에는 소요·남은 시간을 빼고 내보내서, 이 스냅샷으로
|
|
@@ -1656,7 +2456,11 @@ var FlowEngine = class {
|
|
|
1656
2456
|
...t.status === "in-progress" ? { remainingMs: t.remainingMs, startedAtSimMs: t.startedAtSimMs, progress: this.progressOf(t) } : {},
|
|
1657
2457
|
...t.personnel?.length ? { personnel: t.personnel.slice() } : {},
|
|
1658
2458
|
...t.assets?.length ? { assets: t.assets.slice() } : {},
|
|
1659
|
-
...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 } : {}
|
|
1660
2464
|
})),
|
|
1661
2465
|
orders: [...this.orders.values()].map((o) => ({
|
|
1662
2466
|
id: o.id,
|
|
@@ -1666,23 +2470,40 @@ var FlowEngine = class {
|
|
|
1666
2470
|
requested: o.requested,
|
|
1667
2471
|
fulfilled: o.fulfilled,
|
|
1668
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 } : {},
|
|
1669
2476
|
held: o.held
|
|
1670
2477
|
})),
|
|
1671
2478
|
attentions: this.computeAttentions()
|
|
1672
2479
|
};
|
|
1673
2480
|
}
|
|
1674
2481
|
/*
|
|
1675
|
-
* 주목 신호 판단 — 상태(
|
|
2482
|
+
* 주목 신호 판단 — 상태(자리·설비·오더)에서 도메인 조건을 평가해 Attention 방출.
|
|
1676
2483
|
* severity=ISA-18.2 우선순위 계열, kind=도메인 라벨. UI 는 판단 안 함(임계값 여기 소유).
|
|
1677
2484
|
* 도메인별 추가 판단은 서브클래스가 override 로 확장(super.computeAttentions() 합성).
|
|
1678
2485
|
*/
|
|
1679
2486
|
computeAttentions() {
|
|
2487
|
+
const now = this.now();
|
|
1680
2488
|
const out = deriveAttentions(
|
|
1681
|
-
{
|
|
1682
|
-
|
|
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)
|
|
1683
2498
|
);
|
|
1684
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
|
+
}
|
|
1685
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);
|
|
1686
2507
|
return out;
|
|
1687
2508
|
}
|
|
1688
2509
|
/**
|
|
@@ -1709,8 +2530,42 @@ var FlowEngine = class {
|
|
|
1709
2530
|
return clone;
|
|
1710
2531
|
}
|
|
1711
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
|
+
}
|
|
1712
2551
|
now() {
|
|
1713
|
-
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
|
+
};
|
|
1714
2569
|
}
|
|
1715
2570
|
randInt(min, max) {
|
|
1716
2571
|
return max <= min ? min : min + Math.floor(this.rng() * (max - min + 1));
|
|
@@ -1733,7 +2588,7 @@ var FlowEngine = class {
|
|
|
1733
2588
|
*/
|
|
1734
2589
|
apply(envelope) {
|
|
1735
2590
|
if (!this.observer) {
|
|
1736
|
-
this.observer = new ObservedReducer(this.boardDef ?? {
|
|
2591
|
+
this.observer = new ObservedReducer(this.boardDef ?? { locations: [], equipment: [] });
|
|
1737
2592
|
this.observeMode = true;
|
|
1738
2593
|
}
|
|
1739
2594
|
this.observer.apply(envelope);
|
|
@@ -1762,6 +2617,41 @@ var FlowEngine = class {
|
|
|
1762
2617
|
loadOperations(ops = []) {
|
|
1763
2618
|
for (const o of ops) if (o?.key) this.operationSpecs.set(o.key, o);
|
|
1764
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
|
+
}
|
|
1765
2655
|
/**
|
|
1766
2656
|
* task 소요 산출 — 우선순위: **① 추정기(이력 보정) → ② 명세(ISA-95 Duration + 변동) → ③ 도메인 상수.**
|
|
1767
2657
|
*
|
|
@@ -1871,8 +2761,18 @@ var FlowEngine = class {
|
|
|
1871
2761
|
defaultDurations: operations.filter((o) => o.duration === "default").length
|
|
1872
2762
|
};
|
|
1873
2763
|
}
|
|
1874
|
-
/**
|
|
2764
|
+
/**
|
|
2765
|
+
* `skuMix` 에서 weight 로 gtin 선택(rng) — 도착·오더 자극의 품목 결정.
|
|
2766
|
+
*
|
|
2767
|
+
* **빈 목록이면 고르지 않는다**(`undefined`). 예전에는 `mix[mix.length - 1].gtin` 으로 떨어져
|
|
2768
|
+
* `mix[-1]` 이 undefined 가 되고 거기서 던졌다 — 그 예외가 서버까지 올라가 **정확도 추세 전체를
|
|
2769
|
+
* 죽였다**(품목 구성을 선언하지 않은 자극 하나가 예측 전체를 껐다).
|
|
2770
|
+
*
|
|
2771
|
+
* 없는 품목을 지어내지 않는다: 무엇을 만들지 모르면 **만들지 않는 것**이 맞고, 부르는 쪽이
|
|
2772
|
+
* 그 사실을 알고 건너뛴다.
|
|
2773
|
+
*/
|
|
1875
2774
|
pickGtin(mix) {
|
|
2775
|
+
if (!mix?.length) return void 0;
|
|
1876
2776
|
const total = mix.reduce((s, m) => s + m.weight, 0);
|
|
1877
2777
|
let r = this.rng() * total;
|
|
1878
2778
|
for (const m of mix) {
|
|
@@ -1881,28 +2781,28 @@ var FlowEngine = class {
|
|
|
1881
2781
|
}
|
|
1882
2782
|
return mix[mix.length - 1].gtin;
|
|
1883
2783
|
}
|
|
1884
|
-
|
|
1885
|
-
for (const n of this.
|
|
2784
|
+
locationByType(type) {
|
|
2785
|
+
for (const n of this.locations.values()) if (n.type === type) return n;
|
|
1886
2786
|
return void 0;
|
|
1887
2787
|
}
|
|
1888
2788
|
/**
|
|
1889
2789
|
* process 변화 대수(EPCIS TransformationEvent · ISA-95 Material Consumed/Produced · 씬 Processable.transform).
|
|
1890
2790
|
* inputs 소비 → outputs 생산. 입출력 arity 가 곧 대수:
|
|
1891
2791
|
* merge N→1(조립) · split 1→N(분해) · transform 1→1(타입변경) · loss N→0(소실) · gain 0→N(부산물·생성).
|
|
1892
|
-
* 아이템 상태(소비/생산)와
|
|
2792
|
+
* 아이템 상태(소비/생산)와 자리 점유를 갱신하고 계보(TransformationEvent)를 방출한다.
|
|
1893
2793
|
* 도메인은 이 원시만 호출 — onTaskComplete 의 소비/생산/EPCIS 손코딩을 대체.
|
|
1894
2794
|
*/
|
|
1895
2795
|
transform(inputs, outputs, opts) {
|
|
1896
2796
|
for (const epc of inputs) {
|
|
1897
2797
|
const it = this.items.get(epc);
|
|
1898
2798
|
if (!it) continue;
|
|
1899
|
-
const n = this.
|
|
2799
|
+
const n = this.locations.get(it.location);
|
|
1900
2800
|
if (n) n.occupancy--;
|
|
1901
2801
|
this.items.delete(epc);
|
|
1902
2802
|
}
|
|
1903
2803
|
for (const o of outputs) {
|
|
1904
2804
|
this.items.set(o.epc, { epc: o.epc, gtin: o.gtin, qty: o.qty, location: o.location, disposition: o.disposition });
|
|
1905
|
-
const n = this.
|
|
2805
|
+
const n = this.locations.get(o.location);
|
|
1906
2806
|
if (n) n.occupancy++;
|
|
1907
2807
|
}
|
|
1908
2808
|
this.emit(transformationEvent({
|
|
@@ -1976,7 +2876,7 @@ var FlowEngine = class {
|
|
|
1976
2876
|
for (const c of children) {
|
|
1977
2877
|
const it = this.items.get(c);
|
|
1978
2878
|
if (it) {
|
|
1979
|
-
const n = this.
|
|
2879
|
+
const n = this.locations.get(it.location);
|
|
1980
2880
|
if (n) n.occupancy--;
|
|
1981
2881
|
this.items.delete(c);
|
|
1982
2882
|
}
|
|
@@ -1990,7 +2890,7 @@ var FlowEngine = class {
|
|
|
1990
2890
|
}
|
|
1991
2891
|
/**
|
|
1992
2892
|
* containment 분해(EPCIS AggregationEvent DELETE) — 부모(용기)에서 자식들을 풀어냄.
|
|
1993
|
-
* materialize 지정 시 자식이 독립 아이템으로 등장(materialize:
|
|
2893
|
+
* materialize 지정 시 자식이 독립 아이템으로 등장(materialize: 자리 배치 + occupancy + ObjectEvent ADD).
|
|
1994
2894
|
* 미지정 시 이벤트만.
|
|
1995
2895
|
*/
|
|
1996
2896
|
disaggregate(parent, children, opts) {
|
|
@@ -2003,7 +2903,7 @@ var FlowEngine = class {
|
|
|
2003
2903
|
const m = opts.materialize;
|
|
2004
2904
|
for (const c of children) {
|
|
2005
2905
|
this.items.set(c, { epc: c, location: m.location, disposition: m.disposition });
|
|
2006
|
-
const n = this.
|
|
2906
|
+
const n = this.locations.get(m.location);
|
|
2007
2907
|
if (n) n.occupancy++;
|
|
2008
2908
|
this.emit(objectEvent({ eventTime: this.now(), action: "ADD", bizStep: opts.bizStep, disposition: m.disposition, epcList: [c], readPoint: m.location, bizLocation: m.location }));
|
|
2009
2909
|
}
|
|
@@ -2011,22 +2911,22 @@ var FlowEngine = class {
|
|
|
2011
2911
|
}
|
|
2012
2912
|
/** 품질 보고(OEE Quality) — 도메인이 완료 시 자원별 양품/불량 1건 계상(예: 용접 수율). */
|
|
2013
2913
|
recordOutput(moverId, good) {
|
|
2014
|
-
const m = moverId ? this.
|
|
2914
|
+
const m = moverId ? this.equipment.get(moverId) : void 0;
|
|
2015
2915
|
if (!m) return;
|
|
2016
2916
|
if (good) m.goodCount++;
|
|
2017
2917
|
else m.scrapCount++;
|
|
2018
2918
|
this.emitOp(OP_EVENT.quality, { moverId: m.id, good, goodCount: m.goodCount, scrapCount: m.scrapCount });
|
|
2019
2919
|
}
|
|
2020
|
-
/**
|
|
2920
|
+
/** 설비 OEE(스냅샷 파생) — Availability×Performance×Quality. planned = 설비 존재 sim 시간(clockMs). */
|
|
2021
2921
|
oeeOf(m) {
|
|
2022
2922
|
return computeOee(m, this.clockMs);
|
|
2023
2923
|
}
|
|
2024
|
-
/** 정책에 넘길 특정 타입
|
|
2025
|
-
slotViews(
|
|
2924
|
+
/** 정책에 넘길 특정 타입 자리의 관측 뷰 — 예약(그 자리로 향하는 in-flight task) 포함. */
|
|
2925
|
+
slotViews(locationType) {
|
|
2026
2926
|
const reserved = /* @__PURE__ */ new Map();
|
|
2027
2927
|
for (const t of this.tasks.values()) if (t.status !== "completed") reserved.set(t.toNode, (reserved.get(t.toNode) ?? 0) + 1);
|
|
2028
2928
|
const views = [];
|
|
2029
|
-
for (const n of this.
|
|
2929
|
+
for (const n of this.locations.values()) if (n.type === locationType) views.push({ id: n.id, capacity: n.capacity, occupancy: n.occupancy, reserved: reserved.get(n.id) ?? 0 });
|
|
2030
2930
|
return views;
|
|
2031
2931
|
}
|
|
2032
2932
|
emit(event) {
|
|
@@ -2062,21 +2962,26 @@ var FlowEngine = class {
|
|
|
2062
2962
|
...t.personnel?.length ? { personnel: t.personnel.slice() } : {},
|
|
2063
2963
|
...t.assets?.length ? { assets: t.assets.slice() } : {},
|
|
2064
2964
|
...t.resources?.length ? { resources: t.resources.slice() } : {},
|
|
2965
|
+
/* 실제 자재 이동 — 인원·설비와 같은 채널(실적을 한 곳에서 읽는다). */
|
|
2966
|
+
...t.materialActual?.length ? { materialActual: t.materialActual.map((r) => ({ ...r })) } : {},
|
|
2065
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 } : {},
|
|
2066
2971
|
...inProgress ? { remainingMs: t.remainingMs, startedAtSimMs: t.startedAtSimMs, progress: t.durationMs ? done / t.durationMs : void 0 } : {}
|
|
2067
2972
|
});
|
|
2068
2973
|
}
|
|
2069
2974
|
/** 사람 상태 전이 — 설비와 별개 채널(어휘가 다르다: 고장이 아니라 교대·투입). */
|
|
2070
2975
|
emitAsset(a) {
|
|
2071
|
-
this.emitOp(OP_EVENT.asset, { assetId: a.id,
|
|
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) });
|
|
2072
2977
|
}
|
|
2073
2978
|
emitPerson(p) {
|
|
2074
|
-
this.emitOp(OP_EVENT.person, { personId: p.id,
|
|
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) });
|
|
2075
2980
|
}
|
|
2076
2981
|
/** 설비 상태 전이 — `taskId` 를 함께 싣는다(사람·자산 델타와 같은 규칙). 없으면 미러가 "이 설비가
|
|
2077
2982
|
* 무슨 일을 하는 중인가" 를 알 수 없어 작업↔자원 연결이 한쪽에서만 성립한다. */
|
|
2078
|
-
|
|
2079
|
-
this.emitOp(OP_EVENT.equipment, { moverId: m.id, kind: m.kind, status: m.status, location: m.location, taskId: m.taskId ?? void 0, motion });
|
|
2983
|
+
emitEquipment(m, 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) });
|
|
2080
2985
|
}
|
|
2081
2986
|
/** 오더 델타 — **라인까지 싣는다.** 라인이 빠지면 미러가 남은 데맨드를 라인별로 재계획할 수 없다. */
|
|
2082
2987
|
emitOrder(o) {
|
|
@@ -2087,6 +2992,9 @@ var FlowEngine = class {
|
|
|
2087
2992
|
requested: o.requested,
|
|
2088
2993
|
fulfilled: o.fulfilled,
|
|
2089
2994
|
held: o.held,
|
|
2995
|
+
...o.priority !== void 0 ? { priority: o.priority } : {},
|
|
2996
|
+
...o.startTime ? { startTime: o.startTime } : {},
|
|
2997
|
+
...o.endTime ? { endTime: o.endTime } : {},
|
|
2090
2998
|
...o.lines?.length ? { lines: o.lines.map((l) => ({ gtin: l.gtin, requested: l.requested })) } : {}
|
|
2091
2999
|
});
|
|
2092
3000
|
}
|
|
@@ -2156,7 +3064,9 @@ var FlowEngine = class {
|
|
|
2156
3064
|
const want = Math.max(0, Math.floor(req.quantity ?? 0));
|
|
2157
3065
|
if (!want) continue;
|
|
2158
3066
|
const avail = [...this.assets.values()].filter(
|
|
2159
|
-
|
|
3067
|
+
/* 인원과 같은 규칙 — 상속을 타고 닫아 판정하고 유효기간 밖은 제외한다. 요구는 등급 하나+수량. */
|
|
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))
|
|
2160
3070
|
);
|
|
2161
3071
|
if (avail.length < want) return null;
|
|
2162
3072
|
for (let i = 0; i < want; i++) picked.push(avail[i].id);
|
|
@@ -2174,7 +3084,7 @@ var FlowEngine = class {
|
|
|
2174
3084
|
a.taskId = t.id;
|
|
2175
3085
|
if (t.itemEpc) {
|
|
2176
3086
|
a.carrying = t.itemEpc;
|
|
2177
|
-
const it = this.
|
|
3087
|
+
const it = this.itemByRef(t.itemEpc);
|
|
2178
3088
|
if (it) it.carriedBy = a.id;
|
|
2179
3089
|
}
|
|
2180
3090
|
this.emitAsset(a);
|
|
@@ -2205,6 +3115,194 @@ var FlowEngine = class {
|
|
|
2205
3115
|
* 여기서는 고르기만 하고 잡지 않는다: 설비까지 확보된 뒤 `assignCrew` 가 확정한다
|
|
2206
3116
|
* (반쯤 잡고 실패하면 사람이 아무 일도 못 하면서 묶인다).
|
|
2207
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
|
+
}
|
|
2208
3306
|
claimPersonnel(t) {
|
|
2209
3307
|
const need = this.operationSpecs.get(t.kind)?.personnelSpecification;
|
|
2210
3308
|
if (!need?.length) return [];
|
|
@@ -2213,7 +3311,11 @@ var FlowEngine = class {
|
|
|
2213
3311
|
const want = Math.max(0, Math.floor(req.quantity ?? 0));
|
|
2214
3312
|
if (!want) continue;
|
|
2215
3313
|
const avail = [...this.persons.values()].filter(
|
|
2216
|
-
(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
|
+
/* 자격은 **상속을 타고 닫아** 판정한다 — 사람은 여러 등급에 속할 수 있고(표준), 등급은
|
|
3316
|
+
다른 등급을 상속한다. "생산직 2명" 요구를 "용접 자격자" 가 만족해야 한다.
|
|
3317
|
+
유효기간 밖의 등급은 닫힘에서 빠진다(만료된 자격으로 배정되지 않는다). */
|
|
3318
|
+
(req.personnelClass === void 0 || classClosure(p.personnelClassIds, this.classDefs.personnel, this.now()).has(req.personnelClass))
|
|
2217
3319
|
);
|
|
2218
3320
|
if (avail.length < want) return null;
|
|
2219
3321
|
for (let i = 0; i < want; i++) picked.push(avail[i].id);
|
|
@@ -2226,8 +3328,12 @@ var FlowEngine = class {
|
|
|
2226
3328
|
* 여기서는 고르기만 한다 — 확정은 호출부가 다른 자원까지 확보한 뒤에 한다.
|
|
2227
3329
|
*/
|
|
2228
3330
|
claimEquipment(t) {
|
|
2229
|
-
const free = (kind, picked2 = []) => [...this.
|
|
2230
|
-
(m) => m.status === "idle" && !m.held && !this.offShift(m) && !
|
|
3331
|
+
const free = (kind, picked2 = []) => [...this.equipment.values()].filter(
|
|
3332
|
+
(m) => m.status === "idle" && !m.held && !this.offShift(m) && !this.outOfEffect(m) && // 도입 전·폐기 후는 이 시각의 모델에 없다
|
|
3333
|
+
!picked2.includes(m.id) && /* 설비의 소속은 아직 단수(`kind`)다 — 저널에 기록이 쌓여 복수화를 별도 작업으로 두었다.
|
|
3334
|
+
다만 등급 정의가 있으면 **상속은 지금도 탄다**: 'welder-6axis' 가 'welder' 를 상속하면
|
|
3335
|
+
'welder' 요구를 만족한다. 단수/복수와 상속은 다른 축이므로 하나를 기다리지 않는다. */
|
|
3336
|
+
(kind === void 0 || classClosure(m.kind ? [m.kind] : [], this.classDefs.equipment, this.now()).has(kind))
|
|
2231
3337
|
);
|
|
2232
3338
|
const need = this.operationSpecs.get(t.kind)?.equipmentSpecification;
|
|
2233
3339
|
if (!need?.length) {
|
|
@@ -2268,33 +3374,87 @@ var FlowEngine = class {
|
|
|
2268
3374
|
}
|
|
2269
3375
|
/** 사람의 교대 판정 — 자원(offShift)과 같은 규칙. */
|
|
2270
3376
|
personOffShift(p) {
|
|
2271
|
-
|
|
2272
|
-
if (!w) return false;
|
|
2273
|
-
const h = this.hourOfDay();
|
|
2274
|
-
return !(w.startHour <= w.endHour ? h >= w.startHour && h < w.endHour : h >= w.startHour || h < w.endHour);
|
|
3377
|
+
return this.offCalendar(p);
|
|
2275
3378
|
}
|
|
2276
3379
|
/**
|
|
2277
3380
|
* 이 자리가 동시 처리 한도에 찼는가 — `parallelism` 을 선언한 자리만 판정한다(미선언=제약 없음).
|
|
2278
3381
|
* 세는 대상은 **그 자리에서 진행 중인 작업**(`toNode` 기준, in-progress). 대기 중인 작업은 세지 않는다.
|
|
2279
3382
|
*/
|
|
2280
|
-
stationFull(
|
|
2281
|
-
if (!
|
|
2282
|
-
const limit = this.
|
|
3383
|
+
stationFull(locationId) {
|
|
3384
|
+
if (!locationId) return false;
|
|
3385
|
+
const limit = this.locations.get(locationId)?.parallelism;
|
|
2283
3386
|
if (!(typeof limit === "number" && limit > 0)) return false;
|
|
2284
3387
|
let running = 0;
|
|
2285
|
-
for (const t of this.tasks.values()) if (t.status === "in-progress" && t.toNode ===
|
|
3388
|
+
for (const t of this.tasks.values()) if (t.status === "in-progress" && t.toNode === locationId) running++;
|
|
2286
3389
|
return running >= limit;
|
|
2287
3390
|
}
|
|
2288
3391
|
/** 교대 밖인가 — 자원이 지금 일하지 않는 이유 중 고장·계획정지와 구별되는 세 번째. */
|
|
2289
3392
|
offShift(m) {
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
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);
|
|
2294
3454
|
}
|
|
2295
3455
|
/** 시뮬 시각의 시(0..23) — 운영시간·시간대 배율의 기준. */
|
|
2296
3456
|
hourOfDay() {
|
|
2297
|
-
return new Date(
|
|
3457
|
+
return new Date(this.nowMs()).getUTCHours();
|
|
2298
3458
|
}
|
|
2299
3459
|
/**
|
|
2300
3460
|
* 운영시간 안인가 — `window {startHour, endHour}`. **선언만 되고 소비처가 없던 필드**를 판정한다.
|
|
@@ -2312,19 +3472,19 @@ var FlowEngine = class {
|
|
|
2312
3472
|
return -Math.log(1 - this.rng()) * meanMs;
|
|
2313
3473
|
}
|
|
2314
3474
|
/**
|
|
2315
|
-
* 확률적 설비 고장(MTBF/MTTR) — mtbf 지정
|
|
3475
|
+
* 확률적 설비 고장(MTBF/MTTR) — mtbf 지정 설비만 참여(미지정=고장 없음, rng 무소비 → byte-identical).
|
|
2316
3476
|
* up: nextFailure 도래 시 down(수리까지 repairUntil). down: downMs 누적, repair 도래 시 up(다음 고장 예약).
|
|
2317
|
-
* down 중
|
|
3477
|
+
* down 중 설비는 배정 불가 + 진행중 task 동결(processTasks 가 skip) → OEE Availability 손실.
|
|
2318
3478
|
*/
|
|
2319
3479
|
processFailures(dt) {
|
|
2320
|
-
for (const m of this.
|
|
3480
|
+
for (const m of this.equipment.values()) {
|
|
2321
3481
|
if (m.status === "down") {
|
|
2322
3482
|
m.downMs += dt;
|
|
2323
3483
|
if (m.repairUntilMs != null && this.clockMs >= m.repairUntilMs) {
|
|
2324
3484
|
m.status = m.taskId ? "busy" : "idle";
|
|
2325
3485
|
m.repairUntilMs = void 0;
|
|
2326
3486
|
if (m.mtbfMs !== void 0) m.nextFailureMs = this.clockMs + this.sampleExp(m.mtbfMs);
|
|
2327
|
-
this.
|
|
3487
|
+
this.emitEquipment(m);
|
|
2328
3488
|
}
|
|
2329
3489
|
continue;
|
|
2330
3490
|
}
|
|
@@ -2336,7 +3496,7 @@ var FlowEngine = class {
|
|
|
2336
3496
|
m.status = "down";
|
|
2337
3497
|
m.repairUntilMs = this.clockMs + this.sampleExp(m.mttrMs ?? m.mtbfMs);
|
|
2338
3498
|
m.nextFailureMs = void 0;
|
|
2339
|
-
this.
|
|
3499
|
+
this.emitEquipment(m);
|
|
2340
3500
|
}
|
|
2341
3501
|
}
|
|
2342
3502
|
}
|
|
@@ -2354,6 +3514,8 @@ var FlowEngine = class {
|
|
|
2354
3514
|
const gtinKey = parsedClass?.gtinKey ?? parsedSelf.gtinKey;
|
|
2355
3515
|
return {
|
|
2356
3516
|
epc: i.epc,
|
|
3517
|
+
...i.subLotId ? { subLotId: i.subLotId } : {},
|
|
3518
|
+
...i.definitionId ? { definitionId: i.definitionId } : {},
|
|
2357
3519
|
...i.gtin ? { gtin: i.gtin } : {},
|
|
2358
3520
|
...gtinKey ? { gtinKey } : {},
|
|
2359
3521
|
...lot ? { lot } : {},
|
|
@@ -2363,6 +3525,7 @@ var FlowEngine = class {
|
|
|
2363
3525
|
...i.carriedBy ? { carriedBy: i.carriedBy } : {},
|
|
2364
3526
|
...i.qty !== void 0 ? { qty: i.qty } : {},
|
|
2365
3527
|
...i.uom ? { uom: i.uom } : {},
|
|
3528
|
+
...i.quantities?.length ? { quantities: i.quantities } : {},
|
|
2366
3529
|
...i.expiry !== void 0 ? { expiry: i.expiry } : {},
|
|
2367
3530
|
...i.ilmd ? { ilmd: i.ilmd } : {}
|
|
2368
3531
|
};
|
|
@@ -2390,7 +3553,9 @@ var FlowEngine = class {
|
|
|
2390
3553
|
}
|
|
2391
3554
|
}
|
|
2392
3555
|
processOrders() {
|
|
2393
|
-
|
|
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);
|
|
2394
3559
|
}
|
|
2395
3560
|
/**
|
|
2396
3561
|
* 작업 진행 — **진행을 먼저, 배정을 나중에.**
|
|
@@ -2404,54 +3569,87 @@ var FlowEngine = class {
|
|
|
2404
3569
|
this.assignTasks();
|
|
2405
3570
|
}
|
|
2406
3571
|
assignTasks() {
|
|
2407
|
-
|
|
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) {
|
|
2408
3575
|
if (t.status !== "created") continue;
|
|
2409
3576
|
const crew = this.claimPersonnel(t);
|
|
2410
3577
|
if (crew === null) continue;
|
|
2411
3578
|
const gear = this.claimAssets(t);
|
|
2412
3579
|
if (gear === null) continue;
|
|
3580
|
+
const stock = this.claimMaterials(t);
|
|
3581
|
+
if (stock === null) continue;
|
|
2413
3582
|
if (t.intent === "dwell") {
|
|
2414
3583
|
t.status = "in-progress";
|
|
2415
3584
|
t.remainingMs = t.durationMs;
|
|
2416
3585
|
t.startedAtSimMs = this.clockMs;
|
|
2417
3586
|
this.assignCrew(t, crew);
|
|
2418
3587
|
this.assignAssets(t, gear);
|
|
3588
|
+
this.consumeMaterials(t, stock);
|
|
2419
3589
|
this.emitTask(t);
|
|
2420
3590
|
continue;
|
|
2421
3591
|
}
|
|
2422
3592
|
if (this.stationFull(t.toNode)) continue;
|
|
2423
3593
|
const rigs = this.claimEquipment(t);
|
|
2424
3594
|
if (!rigs) continue;
|
|
2425
|
-
const
|
|
2426
|
-
if (t.setupMs && t.changeoverKey !== void 0 &&
|
|
3595
|
+
const eq = this.equipment.get(rigs[0]);
|
|
3596
|
+
if (t.setupMs && t.changeoverKey !== void 0 && eq.lastChangeoverKey !== void 0 && eq.lastChangeoverKey !== t.changeoverKey) {
|
|
2427
3597
|
t.appliedSetupMs = t.setupMs;
|
|
2428
3598
|
t.durationMs += t.setupMs;
|
|
2429
3599
|
}
|
|
2430
|
-
if (t.changeoverKey !== void 0)
|
|
3600
|
+
if (t.changeoverKey !== void 0) eq.lastChangeoverKey = t.changeoverKey;
|
|
2431
3601
|
for (const id of rigs) {
|
|
2432
|
-
const m = this.
|
|
3602
|
+
const m = this.equipment.get(id);
|
|
2433
3603
|
m.status = "busy";
|
|
2434
3604
|
m.taskId = t.id;
|
|
2435
3605
|
}
|
|
2436
3606
|
t.status = "in-progress";
|
|
2437
|
-
t.resource =
|
|
3607
|
+
t.resource = eq.id;
|
|
2438
3608
|
t.remainingMs = t.durationMs;
|
|
2439
3609
|
t.startedAtSimMs = this.clockMs;
|
|
2440
3610
|
if (rigs.length > 1) t.resources = rigs.slice();
|
|
2441
3611
|
this.assignCrew(t, crew);
|
|
2442
3612
|
this.assignAssets(t, gear);
|
|
3613
|
+
this.consumeMaterials(t, stock);
|
|
2443
3614
|
this.emitTask(t);
|
|
2444
|
-
if (t.intent === "process") this.
|
|
2445
|
-
else this.
|
|
3615
|
+
if (t.intent === "process") this.emitEquipment(eq);
|
|
3616
|
+
else this.emitEquipment(eq, { fromNode: t.fromNode, toNode: t.toNode, startedAtSimMs: this.clockMs, durationMs: t.durationMs, progress: 0, elapsedMs: 0 });
|
|
2446
3617
|
}
|
|
2447
3618
|
}
|
|
2448
3619
|
/** in-progress 진행/완료 — 상태 효과는 전부 도메인(onTaskComplete). base 는 자원 해제·델타만. */
|
|
2449
3620
|
advanceTasks(dt) {
|
|
2450
3621
|
for (const t of this.tasks.values()) {
|
|
2451
3622
|
if (t.status !== "in-progress") continue;
|
|
2452
|
-
if (t.resource && this.
|
|
3623
|
+
if (t.resource && this.equipment.get(t.resource)?.status === "down") continue;
|
|
2453
3624
|
t.remainingMs -= dt;
|
|
2454
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);
|
|
2455
3653
|
this.onTaskComplete(t);
|
|
2456
3654
|
t.status = "completed";
|
|
2457
3655
|
this.releaseCrew(t);
|
|
@@ -2460,26 +3658,26 @@ var FlowEngine = class {
|
|
|
2460
3658
|
this.emitTask(t);
|
|
2461
3659
|
continue;
|
|
2462
3660
|
}
|
|
2463
|
-
const
|
|
3661
|
+
const eq = this.equipment.get(t.resource);
|
|
2464
3662
|
const setup = t.appliedSetupMs ?? 0;
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
if (t.intent !== "process")
|
|
3663
|
+
eq.setupMs += setup;
|
|
3664
|
+
eq.runMs += t.durationMs - setup;
|
|
3665
|
+
eq.status = "idle";
|
|
3666
|
+
eq.taskId = null;
|
|
3667
|
+
if (t.intent !== "process") eq.location = t.toNode;
|
|
2470
3668
|
for (const id of t.resources ?? []) {
|
|
2471
|
-
if (id ===
|
|
2472
|
-
const m = this.
|
|
3669
|
+
if (id === eq.id) continue;
|
|
3670
|
+
const m = this.equipment.get(id);
|
|
2473
3671
|
if (!m) continue;
|
|
2474
3672
|
m.setupMs += setup;
|
|
2475
3673
|
m.runMs += t.durationMs - setup;
|
|
2476
3674
|
m.status = "idle";
|
|
2477
3675
|
m.taskId = null;
|
|
2478
3676
|
if (t.intent !== "process") m.location = t.toNode;
|
|
2479
|
-
this.
|
|
3677
|
+
this.emitEquipment(m);
|
|
2480
3678
|
}
|
|
2481
3679
|
this.emitTask(t);
|
|
2482
|
-
this.
|
|
3680
|
+
this.emitEquipment(eq);
|
|
2483
3681
|
}
|
|
2484
3682
|
}
|
|
2485
3683
|
};
|
|
@@ -2496,10 +3694,11 @@ var WmsKernel = class extends FlowEngine {
|
|
|
2496
3694
|
}
|
|
2497
3695
|
/** 입고 도착 — §4 라이프사이클: ASN(PO) → 팔레트 조립 → 수령 → putaway task. */
|
|
2498
3696
|
onArrival(spec) {
|
|
2499
|
-
const dock = this.
|
|
3697
|
+
const dock = this.locationByType("dock");
|
|
2500
3698
|
if (!dock) return;
|
|
2501
3699
|
const epc = ssccUri(COMPANY_PREFIX, ++this.epcSeq);
|
|
2502
3700
|
const gtin = this.pickGtin(spec.content.skuMix);
|
|
3701
|
+
if (!gtin) return;
|
|
2503
3702
|
const qty = this.randInt(spec.content.qtyPerLine.min, spec.content.qtyPerLine.max);
|
|
2504
3703
|
const po = gdtiUri(COMPANY_PREFIX, "401", ++this.poSeq);
|
|
2505
3704
|
const eventTime = this.now();
|
|
@@ -2537,7 +3736,9 @@ var WmsKernel = class extends FlowEngine {
|
|
|
2537
3736
|
onOrder(spec) {
|
|
2538
3737
|
const lpl = spec.content.linesPerOrder;
|
|
2539
3738
|
if (!lpl) {
|
|
2540
|
-
|
|
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);
|
|
2541
3742
|
return;
|
|
2542
3743
|
}
|
|
2543
3744
|
const n = Math.min(this.randInt(lpl.min, lpl.max), spec.content.skuMix.length);
|
|
@@ -2545,10 +3746,12 @@ var WmsKernel = class extends FlowEngine {
|
|
|
2545
3746
|
let pool = spec.content.skuMix;
|
|
2546
3747
|
for (let i = 0; i < n && pool.length > 0; i++) {
|
|
2547
3748
|
const gtin = this.pickGtin(pool);
|
|
3749
|
+
if (!gtin) break;
|
|
2548
3750
|
lines.push({ gtin, qty: this.randInt(spec.content.qtyPerLine.min, spec.content.qtyPerLine.max) });
|
|
2549
3751
|
pool = pool.filter((m) => m.gtin !== gtin);
|
|
2550
3752
|
}
|
|
2551
|
-
|
|
3753
|
+
if (!lines.length) return;
|
|
3754
|
+
this.createSalesOrder(lines, spec);
|
|
2552
3755
|
}
|
|
2553
3756
|
/** Command 채널 — 오더 즉시 투입(트랜잭션 프론트엔드/prescriptive). */
|
|
2554
3757
|
handleCommand(cmd) {
|
|
@@ -2561,7 +3764,12 @@ var WmsKernel = class extends FlowEngine {
|
|
|
2561
3764
|
}
|
|
2562
3765
|
return super.handleCommand(cmd);
|
|
2563
3766
|
}
|
|
2564
|
-
|
|
3767
|
+
/**
|
|
3768
|
+
* 오더 생성 — 약속(`promised`)이 있으면 **납기와 우선순위를 함께 싣는다**(표준 `EndTime`·`Priority`).
|
|
3769
|
+
* 없으면 싣지 않는다: 없는 약속을 만들면 그 뒤 모든 지연 판정이 거짓 위에 선다.
|
|
3770
|
+
*/
|
|
3771
|
+
/** 오더 생성 — 약속(납기·우선순위)은 base `promiseOf` 가 계산한다(도메인마다 다르게 재지 않는다). */
|
|
3772
|
+
createSalesOrder(lines, spec) {
|
|
2565
3773
|
const id = `order-${++this.orderSeq}`;
|
|
2566
3774
|
const so = gdtiUri(COMPANY_PREFIX, "402", ++this.soSeq);
|
|
2567
3775
|
const requested = lines.reduce((s, l) => s + l.qty, 0);
|
|
@@ -2575,7 +3783,8 @@ var WmsKernel = class extends FlowEngine {
|
|
|
2575
3783
|
allocated: [],
|
|
2576
3784
|
picked: [],
|
|
2577
3785
|
shipmentEpc: null,
|
|
2578
|
-
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) : {}
|
|
2579
3788
|
};
|
|
2580
3789
|
this.orders.set(id, order);
|
|
2581
3790
|
this.emitOrder(order);
|
|
@@ -2586,14 +3795,14 @@ var WmsKernel = class extends FlowEngine {
|
|
|
2586
3795
|
* 멀티SKU 는 여러 라인의 팔레트를 한 오더로 모아 단일 출하(finalizeOrder 통합 화물).
|
|
2587
3796
|
*/
|
|
2588
3797
|
allocate(o) {
|
|
2589
|
-
const staging = this.
|
|
3798
|
+
const staging = this.locationByType("staging");
|
|
2590
3799
|
if (!staging || !o.lines) return;
|
|
2591
3800
|
const chosenAll = [];
|
|
2592
3801
|
for (const line of o.lines) {
|
|
2593
3802
|
const already = o.allocated.filter((e) => this.items.get(e)?.gtin === line.gtin).length;
|
|
2594
3803
|
const need = line.requested - already;
|
|
2595
3804
|
if (need <= 0) continue;
|
|
2596
|
-
const available = [...this.items.values()].filter((i) => i.gtin === line.gtin && i.disposition === DISP.sellable && this.
|
|
3805
|
+
const available = [...this.items.values()].filter((i) => i.gtin === line.gtin && i.disposition === DISP.sellable && this.locations.get(i.location)?.type === "storage").map((i) => ({ epc: i.epc, location: i.location, qty: i.qty ?? 1, expiry: i.expiry }));
|
|
2597
3806
|
const chosen = this.policy.selectStock({ gtin: line.gtin, qty: need, available });
|
|
2598
3807
|
for (const epc of chosen) {
|
|
2599
3808
|
o.allocated.push(epc);
|
|
@@ -2615,9 +3824,10 @@ var WmsKernel = class extends FlowEngine {
|
|
|
2615
3824
|
}
|
|
2616
3825
|
/** 태스크 완료 — 이동 반영 후 putaway=storing, pick=picking(+전량 시 pack→stage→ship). */
|
|
2617
3826
|
onTaskComplete(t) {
|
|
2618
|
-
const item = this.
|
|
2619
|
-
|
|
2620
|
-
const
|
|
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`);
|
|
3829
|
+
const from = this.locations.get(t.fromNode);
|
|
3830
|
+
const to = this.locations.get(t.toNode);
|
|
2621
3831
|
from.occupancy--;
|
|
2622
3832
|
to.occupancy++;
|
|
2623
3833
|
item.location = to.id;
|
|
@@ -2635,7 +3845,7 @@ var WmsKernel = class extends FlowEngine {
|
|
|
2635
3845
|
}
|
|
2636
3846
|
/** 전량 피킹 → packing(조립)·staging·shipping 마감. 화물 사이트 이탈, 백오더 잔량 재할당. */
|
|
2637
3847
|
finalizeOrder(order, staging) {
|
|
2638
|
-
const shipDock = this.
|
|
3848
|
+
const shipDock = this.locationByType("dock-ship") ?? staging;
|
|
2639
3849
|
const eventTime = this.now();
|
|
2640
3850
|
const shipment = ssccUri(COMPANY_PREFIX, ++this.epcSeq);
|
|
2641
3851
|
order.shipmentEpc = shipment;
|
|
@@ -2668,6 +3878,7 @@ var WmsKernel = class extends FlowEngine {
|
|
|
2668
3878
|
var TRAVEL_MS2 = 2e4;
|
|
2669
3879
|
var DWELL_MS = 3e4;
|
|
2670
3880
|
var WINDOW_DELAY_MS = 4e4;
|
|
3881
|
+
var WINDOW_LENGTH_MS = 20 * 6e4;
|
|
2671
3882
|
var CARGO_PER_TRAILER = 2;
|
|
2672
3883
|
var CP = "0614141";
|
|
2673
3884
|
var YmsKernel = class extends FlowEngine {
|
|
@@ -2685,16 +3896,16 @@ var YmsKernel = class extends FlowEngine {
|
|
|
2685
3896
|
}
|
|
2686
3897
|
/** 인바운드(하차) 자극 — 화물 적재된 트레일러 도착. */
|
|
2687
3898
|
onArrival(_spec) {
|
|
2688
|
-
this.spawnAppointment("appointment");
|
|
3899
|
+
this.spawnAppointment("appointment", _spec);
|
|
2689
3900
|
}
|
|
2690
3901
|
/** 아웃바운드(상차) 자극 — 빈 트레일러 도착(도크에서 staging 화물 적재 예정). */
|
|
2691
3902
|
onOrder(_spec) {
|
|
2692
|
-
this.spawnAppointment("appointment-out");
|
|
3903
|
+
this.spawnAppointment("appointment-out", _spec);
|
|
2693
3904
|
}
|
|
2694
3905
|
/** 게이트-인 — 트레일러 도착 → 어포인트먼트(도크도어 배정 + 창). drop 은 야드 주차 태스크, live 는 게이트 대기. */
|
|
2695
|
-
spawnAppointment(kind) {
|
|
2696
|
-
const gate = this.
|
|
2697
|
-
const doors = [...this.
|
|
3906
|
+
spawnAppointment(kind, spec) {
|
|
3907
|
+
const gate = this.locationByType("gate");
|
|
3908
|
+
const doors = [...this.locations.values()].filter((n) => n.type === "dock-door").sort((a, b) => a.id.localeCompare(b.id));
|
|
2698
3909
|
if (!gate || doors.length === 0) return;
|
|
2699
3910
|
const inbound = kind === "appointment";
|
|
2700
3911
|
const epc = graiUri(CP, "10", ++this.epcSeq);
|
|
@@ -2709,7 +3920,24 @@ var YmsKernel = class extends FlowEngine {
|
|
|
2709
3920
|
const door = doors[this.doorRR++ % doors.length];
|
|
2710
3921
|
const id = `order-${++this.orderSeq}`;
|
|
2711
3922
|
const appt = gdtiUri(CP, "404", ++this.soSeq);
|
|
2712
|
-
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
|
+
};
|
|
2713
3941
|
this.orders.set(id, order);
|
|
2714
3942
|
this.apptMode.set(id, this.mode);
|
|
2715
3943
|
this.emitOrder(order);
|
|
@@ -2731,15 +3959,15 @@ var YmsKernel = class extends FlowEngine {
|
|
|
2731
3959
|
const trailer = this.items.get(o.allocated[0]);
|
|
2732
3960
|
if (!trailer) return;
|
|
2733
3961
|
if (this.clockMs < (o.windowStartMs ?? 0)) return;
|
|
2734
|
-
const door = this.
|
|
3962
|
+
const door = this.locations.get(o.dockDoor);
|
|
2735
3963
|
const inflight = [...this.tasks.values()].some((t) => t.status !== "completed" && t.toNode === door.id);
|
|
2736
3964
|
if (door.occupancy > 0 || inflight) return;
|
|
2737
3965
|
const mode = this.apptMode.get(o.id) ?? "drop";
|
|
2738
|
-
if (mode === "drop" && this.
|
|
3966
|
+
if (mode === "drop" && this.locations.get(trailer.location)?.type !== "yard-slot") return;
|
|
2739
3967
|
const outbound = o.kind === "appointment-out";
|
|
2740
3968
|
const bizStep = outbound ? YARD_BIZSTEP.loading : YARD_BIZSTEP.unloading;
|
|
2741
3969
|
if (outbound) {
|
|
2742
|
-
const staging = this.
|
|
3970
|
+
const staging = this.locationByType("staging");
|
|
2743
3971
|
const reserved = new Set([...this.trailerCargo.values()].flat());
|
|
2744
3972
|
const avail = [...this.items.values()].filter((i) => i.location === staging?.id && i.disposition === DISP.sellable && !reserved.has(i.epc)).sort((a, b) => a.epc.localeCompare(b.epc));
|
|
2745
3973
|
if (!staging || avail.length < CARGO_PER_TRAILER) return;
|
|
@@ -2756,14 +3984,15 @@ var YmsKernel = class extends FlowEngine {
|
|
|
2756
3984
|
}
|
|
2757
3985
|
/** 태스크 완료 — 목적지 타입으로 분기: yard-slot=주차, dock-door=상/하차+depart, gate=출차. */
|
|
2758
3986
|
onTaskComplete(t) {
|
|
2759
|
-
const trailer = this.
|
|
2760
|
-
|
|
2761
|
-
const
|
|
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`);
|
|
3989
|
+
const from = this.locations.get(t.fromNode);
|
|
3990
|
+
const to = this.locations.get(t.toNode);
|
|
2762
3991
|
from.occupancy--;
|
|
2763
3992
|
to.occupancy++;
|
|
2764
3993
|
trailer.location = to.id;
|
|
2765
3994
|
if (t.kind === "dwell") {
|
|
2766
|
-
const gate = this.
|
|
3995
|
+
const gate = this.locationByType("gate");
|
|
2767
3996
|
const dep = { id: `task-${++this.taskSeq}`, kind: "depart", status: "created", itemEpc: trailer.epc, fromNode: t.toNode, toNode: gate.id, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: "depart", fromNode: t.toNode, toNode: gate.id }, TRAVEL_MS2), orderId: t.orderId };
|
|
2768
3997
|
this.tasks.set(dep.id, dep);
|
|
2769
3998
|
this.emitTask(dep);
|
|
@@ -2777,7 +4006,7 @@ var YmsKernel = class extends FlowEngine {
|
|
|
2777
4006
|
if (to.type === "dock-door") {
|
|
2778
4007
|
const order2 = t.orderId ? this.orders.get(t.orderId) : void 0;
|
|
2779
4008
|
const outbound = order2?.kind === "appointment-out";
|
|
2780
|
-
const staging = this.
|
|
4009
|
+
const staging = this.locationByType("staging");
|
|
2781
4010
|
const cargo = this.trailerCargo.get(trailer.epc) ?? [];
|
|
2782
4011
|
this.observeDisposition([trailer.epc], DISP.in_progress, outbound ? YARD_BIZSTEP.loading : YARD_BIZSTEP.unloading, to.id);
|
|
2783
4012
|
if (outbound) {
|
|
@@ -2829,10 +4058,10 @@ var MES_PART_GTINS = { partA: PART_A.gtin, partB: PART_B.gtin };
|
|
|
2829
4058
|
var MES_PRODUCT_GTINS = { p1: PRODUCTS[0].gtin, p2: PRODUCTS[1].gtin };
|
|
2830
4059
|
var MES_PRODUCTS = PRODUCTS.map((p) => ({ gtin: p.gtin, label: p.key }));
|
|
2831
4060
|
var ROUTE = [
|
|
2832
|
-
{ kind: "cut",
|
|
2833
|
-
{ kind: "weld",
|
|
2834
|
-
{ kind: "paint",
|
|
2835
|
-
{ kind: "assembly",
|
|
4061
|
+
{ kind: "cut", locationType: "cut-station", resource: "cutter" },
|
|
4062
|
+
{ kind: "weld", locationType: "weld-station", resource: "welder" },
|
|
4063
|
+
{ kind: "paint", locationType: "paint-booth", resource: "painter" },
|
|
4064
|
+
{ kind: "assembly", locationType: "assembly-line", resource: "assembler" }
|
|
2836
4065
|
];
|
|
2837
4066
|
var MesKernel = class extends FlowEngine {
|
|
2838
4067
|
wipSeq = 0;
|
|
@@ -2842,7 +4071,29 @@ var MesKernel = class extends FlowEngine {
|
|
|
2842
4071
|
constructor(tenantId, policy = firstFitPolicy, mesSpec) {
|
|
2843
4072
|
super(tenantId, policy);
|
|
2844
4073
|
this.mesSpec = mesSpec;
|
|
2845
|
-
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
|
+
);
|
|
2846
4097
|
}
|
|
2847
4098
|
productOf(gtin) {
|
|
2848
4099
|
return PRODUCTS.find((p) => p.gtin === gtin);
|
|
@@ -2857,12 +4108,12 @@ var MesKernel = class extends FlowEngine {
|
|
|
2857
4108
|
if (cmd.type === MES_CMD.changeover) {
|
|
2858
4109
|
const a = cmd.args;
|
|
2859
4110
|
if (!a?.resourceId || !a?.gtin) return { commandId: cmd.commandId, accepted: false, errorCode: "changeover-needs-args", error: "mes.changeover: resourceId and gtin required" };
|
|
2860
|
-
const m = this.
|
|
4111
|
+
const m = this.equipment.get(a.resourceId);
|
|
2861
4112
|
if (!m) return { commandId: cmd.commandId, accepted: false, errorCode: "resource-not-found", errorParams: { resourceId: a.resourceId }, error: `resource-not-found: ${a.resourceId}` };
|
|
2862
4113
|
if (m.lastChangeoverKey !== a.gtin) {
|
|
2863
4114
|
m.setupMs += DEFAULT_SETUP_MS;
|
|
2864
4115
|
m.lastChangeoverKey = a.gtin;
|
|
2865
|
-
this.
|
|
4116
|
+
this.emitEquipment(m);
|
|
2866
4117
|
}
|
|
2867
4118
|
return { commandId: cmd.commandId, accepted: true };
|
|
2868
4119
|
}
|
|
@@ -2871,7 +4122,7 @@ var MesKernel = class extends FlowEngine {
|
|
|
2871
4122
|
/** 부품 수령(다품종) — skuMix 의 gtin 으로 부품 종류 결정. */
|
|
2872
4123
|
onArrival(spec) {
|
|
2873
4124
|
if (this.mesSpec) return this.onArrivalDef(spec);
|
|
2874
|
-
const rawStore = this.
|
|
4125
|
+
const rawStore = this.locationByType("raw-store");
|
|
2875
4126
|
if (!rawStore) return;
|
|
2876
4127
|
const gtin = this.pickGtin(spec.content.skuMix);
|
|
2877
4128
|
const part = [PART_A, PART_B].find((p) => p.gtin === gtin);
|
|
@@ -2887,7 +4138,7 @@ var MesKernel = class extends FlowEngine {
|
|
|
2887
4138
|
const product = PRODUCTS[this.orderSeq % PRODUCTS.length];
|
|
2888
4139
|
const id = `order-${++this.orderSeq}`;
|
|
2889
4140
|
const wo = gdtiUri(CP2, "403", ++this.soSeq);
|
|
2890
|
-
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) };
|
|
2891
4142
|
this.orders.set(id, order);
|
|
2892
4143
|
this.emitOrder(order);
|
|
2893
4144
|
}
|
|
@@ -2895,12 +4146,12 @@ var MesKernel = class extends FlowEngine {
|
|
|
2895
4146
|
allocate(o) {
|
|
2896
4147
|
if (this.mesSpec) return this.allocateDef(o);
|
|
2897
4148
|
const s0 = ROUTE[0];
|
|
2898
|
-
const first = this.
|
|
4149
|
+
const first = this.locationByType(s0.locationType);
|
|
2899
4150
|
const product = this.productOf(o.gtin);
|
|
2900
4151
|
if (!first || !product) return;
|
|
2901
4152
|
const picks = [];
|
|
2902
4153
|
for (const line of product.bom) {
|
|
2903
|
-
const available = [...this.items.values()].filter((i) => i.gtin === line.part.gtin && i.disposition === DISP.sellable && this.
|
|
4154
|
+
const available = [...this.items.values()].filter((i) => i.gtin === line.part.gtin && i.disposition === DISP.sellable && this.locations.get(i.location)?.type === "raw-store").map((i) => ({ epc: i.epc, location: i.location, qty: 1 }));
|
|
2904
4155
|
const chosen = this.policy.selectStock({ gtin: line.part.gtin, qty: line.qty, available });
|
|
2905
4156
|
if (chosen.length < line.qty) return;
|
|
2906
4157
|
picks.push(...chosen);
|
|
@@ -2914,23 +4165,36 @@ var MesKernel = class extends FlowEngine {
|
|
|
2914
4165
|
}
|
|
2915
4166
|
/** 라우트 스테이션 태스크 발행(공통) — 제자리 가공(process), 이종 자원, 제품 전환 셋업. */
|
|
2916
4167
|
emitStation(o, stage, itemEpc, changeoverKey) {
|
|
2917
|
-
const
|
|
2918
|
-
const task = { id: `task-${++this.taskSeq}`, kind: stage.kind, status: "created", itemEpc, fromNode:
|
|
4168
|
+
const loc = this.locationByType(stage.locationType);
|
|
4169
|
+
const task = { id: `task-${++this.taskSeq}`, kind: stage.kind, status: "created", itemEpc, fromNode: loc.id, toNode: loc.id, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: stage.kind, fromNode: loc.id, toNode: loc.id, resourceKind: stage.resource }, DEFAULT_CYCLE_MS), orderId: o.id, resourceType: stage.resource, changeoverKey, setupMs: this.paramDuration(stage.kind, OP_PARAM.setupDuration) ?? DEFAULT_SETUP_MS, intent: "process" };
|
|
2919
4170
|
this.tasks.set(task.id, task);
|
|
2920
4171
|
this.emitTask(task);
|
|
2921
4172
|
}
|
|
2922
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
|
+
}
|
|
2923
4186
|
onTaskComplete(t) {
|
|
2924
4187
|
if (this.mesSpec) return this.onTaskCompleteDef(t);
|
|
2925
4188
|
const order = this.orders.get(t.orderId);
|
|
2926
|
-
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`);
|
|
2927
4191
|
const i = ROUTE.findIndex((s) => s.kind === t.kind);
|
|
2928
|
-
const
|
|
4192
|
+
const loc = this.locations.get(t.toNode);
|
|
2929
4193
|
const isLast = i === ROUTE.length - 1;
|
|
2930
4194
|
if (!isLast) {
|
|
2931
4195
|
const inputs = order.allocated.slice();
|
|
2932
4196
|
const wip2 = sgtinUri(CP2, WIP_ITEMREF, ++this.wipSeq);
|
|
2933
|
-
this.transform(inputs, [{ epc: wip2, gtin: WIP_GTIN, qty: 1, location:
|
|
4197
|
+
this.transform(inputs, [{ epc: wip2, gtin: WIP_GTIN, qty: 1, location: loc.id, disposition: DISP.in_progress }], { bizStep: MES_BIZSTEP.producing, disposition: DISP.in_progress, transformationId: order.bizTransaction, readPoint: loc.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
|
|
2934
4198
|
order.allocated = [wip2];
|
|
2935
4199
|
const next = ROUTE[i + 1];
|
|
2936
4200
|
this.emitStation(order, next, wip2, product.gtin);
|
|
@@ -2938,13 +4202,13 @@ var MesKernel = class extends FlowEngine {
|
|
|
2938
4202
|
this.emitOrder(order);
|
|
2939
4203
|
return;
|
|
2940
4204
|
}
|
|
2941
|
-
const fgStore = this.
|
|
4205
|
+
const fgStore = this.locationByType("fg-store");
|
|
2942
4206
|
const wip = order.allocated[0];
|
|
2943
4207
|
const good = this.rng() < (this.paramNumber(t.kind, OP_PARAM.yield) ?? DEFAULT_YIELD);
|
|
2944
4208
|
this.recordOutput(t.resource, good);
|
|
2945
4209
|
const disp = good ? DISP.sellable : DISP.non_sellable;
|
|
2946
4210
|
const outputEpc = sgtinUri(CP2, product.ref, ++this.prodSeq);
|
|
2947
|
-
this.transform([wip], [{ epc: outputEpc, gtin: product.gtin, qty: 1, location: fgStore.id, disposition: disp }], { bizStep: MES_BIZSTEP.producing, disposition: disp, transformationId: order.bizTransaction, readPoint:
|
|
4211
|
+
this.transform([wip], [{ epc: outputEpc, gtin: product.gtin, qty: 1, location: fgStore.id, disposition: disp }], { bizStep: MES_BIZSTEP.producing, disposition: disp, transformationId: order.bizTransaction, readPoint: loc.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
|
|
2948
4212
|
this.emit(objectEvent({ eventTime: this.now(), action: "OBSERVE", bizStep: MES_BIZSTEP.storing, disposition: disp, epcList: [outputEpc], quantityList: [{ epcClass: product.gtin, quantity: 1 }], readPoint: fgStore.id, bizLocation: fgStore.id }));
|
|
2949
4213
|
order.allocated = [];
|
|
2950
4214
|
order.fulfilled = 1;
|
|
@@ -2963,6 +4227,17 @@ var MesKernel = class extends FlowEngine {
|
|
|
2963
4227
|
serialOf(materialKey, serial) {
|
|
2964
4228
|
return sgtinUri(this.mesSpec.companyPrefix, this.mesSpec.binding[materialKey], serial);
|
|
2965
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
|
+
}
|
|
2966
4241
|
/** recipe.route → 오퍼레이션 시퀀스 해소. */
|
|
2967
4242
|
routeOps() {
|
|
2968
4243
|
const d = this.mesSpec.definition;
|
|
@@ -2971,9 +4246,10 @@ var MesKernel = class extends FlowEngine {
|
|
|
2971
4246
|
}
|
|
2972
4247
|
/** 정의 모드 수령 — skuMix gtin 이 레시피 입력 자재면 raw-store 에 생성. */
|
|
2973
4248
|
onArrivalDef(spec) {
|
|
2974
|
-
const rawStore = this.
|
|
4249
|
+
const rawStore = this.locationByType("raw-store");
|
|
2975
4250
|
if (!rawStore) return;
|
|
2976
4251
|
const gtin = this.pickGtin(spec.content.skuMix);
|
|
4252
|
+
if (!gtin) return;
|
|
2977
4253
|
const inputKey = this.recipeDef().inputs.map((i) => i.material).find((k) => this.classOf(k) === gtin);
|
|
2978
4254
|
if (!inputKey) return;
|
|
2979
4255
|
const epc = this.serialOf(inputKey, ++this.epcSeq);
|
|
@@ -2986,19 +4262,19 @@ var MesKernel = class extends FlowEngine {
|
|
|
2986
4262
|
const rc = this.recipeDef();
|
|
2987
4263
|
const id = `order-${++this.orderSeq}`;
|
|
2988
4264
|
const wo = gdtiUri(this.mesSpec.companyPrefix, "403", ++this.soSeq);
|
|
2989
|
-
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) };
|
|
2990
4266
|
this.orders.set(id, order);
|
|
2991
4267
|
this.emitOrder(order);
|
|
2992
4268
|
}
|
|
2993
4269
|
/** 정의 모드 할당 — 레시피 입력 BOM 전량 확보 후 첫 라우트 스텝 태스크. */
|
|
2994
4270
|
allocateDef(o) {
|
|
2995
4271
|
const ops = this.routeOps();
|
|
2996
|
-
if (!ops.length || !ops[0].
|
|
4272
|
+
if (!ops.length || !ops[0].locationType || !this.locationByType(ops[0].locationType)) return;
|
|
2997
4273
|
const rc = this.recipeDef();
|
|
2998
4274
|
const picks = [];
|
|
2999
4275
|
for (const line of rc.inputs) {
|
|
3000
4276
|
const g = this.classOf(line.material);
|
|
3001
|
-
const available = [...this.items.values()].filter((i) => i.gtin === g && i.disposition === DISP.sellable && this.
|
|
4277
|
+
const available = [...this.items.values()].filter((i) => i.gtin === g && i.disposition === DISP.sellable && this.locations.get(i.location)?.type === "raw-store").map((i) => ({ epc: i.epc, location: i.location, qty: 1 }));
|
|
3002
4278
|
const chosen = this.policy.selectStock({ gtin: g, qty: line.qty, available });
|
|
3003
4279
|
if (chosen.length < line.qty) return;
|
|
3004
4280
|
picks.push(...chosen);
|
|
@@ -3011,25 +4287,26 @@ var MesKernel = class extends FlowEngine {
|
|
|
3011
4287
|
this.emitOrder(o);
|
|
3012
4288
|
}
|
|
3013
4289
|
emitStationDef(o, op, itemEpc) {
|
|
3014
|
-
const
|
|
3015
|
-
const task = { id: `task-${++this.taskSeq}`, kind: op.key, status: "created", itemEpc, fromNode:
|
|
4290
|
+
const loc = this.locationByType(op.locationType);
|
|
4291
|
+
const task = { id: `task-${++this.taskSeq}`, kind: op.key, status: "created", itemEpc, fromNode: loc.id, toNode: loc.id, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: op.key, fromNode: loc.id, toNode: loc.id, resourceKind: op.resourceType }, DEFAULT_CYCLE_MS), orderId: o.id, resourceType: op.resourceType, changeoverKey: o.gtin, setupMs: this.paramDuration(op.key, OP_PARAM.setupDuration) ?? DEFAULT_SETUP_MS, intent: op.intent };
|
|
3016
4292
|
this.tasks.set(task.id, task);
|
|
3017
4293
|
this.emitTask(task);
|
|
3018
4294
|
}
|
|
3019
4295
|
/** 정의 모드 완료 — 라우트 인덱스: 중간=WIP 변환+다음 스텝, 마지막=완제품(수율). */
|
|
3020
4296
|
onTaskCompleteDef(t) {
|
|
3021
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`);
|
|
3022
4299
|
const rc = this.recipeDef();
|
|
3023
4300
|
const ops = this.routeOps();
|
|
3024
4301
|
const i = ops.findIndex((s) => s.key === t.kind);
|
|
3025
|
-
const
|
|
4302
|
+
const loc = this.locations.get(t.toNode);
|
|
3026
4303
|
const bizStep = ops[i]?.bizStep ?? MES_BIZSTEP.producing;
|
|
3027
4304
|
const isLast = i === ops.length - 1;
|
|
3028
4305
|
if (!isLast) {
|
|
3029
4306
|
const inputs = order.allocated.slice();
|
|
3030
4307
|
const wip2 = sgtinUri(this.mesSpec.companyPrefix, "WIP", ++this.wipSeq);
|
|
3031
4308
|
const wipGtin = sgtinClass(this.mesSpec.companyPrefix, "WIP");
|
|
3032
|
-
this.transform(inputs, [{ epc: wip2, gtin: wipGtin, qty: 1, location:
|
|
4309
|
+
this.transform(inputs, [{ epc: wip2, gtin: wipGtin, qty: 1, location: loc.id, disposition: DISP.in_progress }], { bizStep, disposition: DISP.in_progress, transformationId: order.bizTransaction, readPoint: loc.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
|
|
3033
4310
|
order.allocated = [wip2];
|
|
3034
4311
|
const next = ops[i + 1];
|
|
3035
4312
|
this.emitStationDef(order, next, wip2);
|
|
@@ -3037,14 +4314,14 @@ var MesKernel = class extends FlowEngine {
|
|
|
3037
4314
|
this.emitOrder(order);
|
|
3038
4315
|
return;
|
|
3039
4316
|
}
|
|
3040
|
-
const fgStore = this.
|
|
4317
|
+
const fgStore = this.locationByType("fg-store");
|
|
3041
4318
|
const wip = order.allocated[0];
|
|
3042
4319
|
const good = this.rng() < (this.paramNumber(t.kind, OP_PARAM.yield) ?? DEFAULT_YIELD);
|
|
3043
4320
|
this.recordOutput(t.resource, good);
|
|
3044
4321
|
const disp = good ? DISP.sellable : DISP.non_sellable;
|
|
3045
4322
|
const outEpc = this.serialOf(rc.outputs[0].material, ++this.prodSeq);
|
|
3046
4323
|
const outGtin = this.classOf(rc.outputs[0].material);
|
|
3047
|
-
this.transform([wip], [{ epc: outEpc, gtin: outGtin, qty: 1, location: fgStore.id, disposition: disp }], { bizStep, disposition: disp, transformationId: order.bizTransaction, readPoint:
|
|
4324
|
+
this.transform([wip], [{ epc: outEpc, gtin: outGtin, qty: 1, location: fgStore.id, disposition: disp }], { bizStep, disposition: disp, transformationId: order.bizTransaction, readPoint: loc.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
|
|
3048
4325
|
this.emit(objectEvent({ eventTime: this.now(), action: "OBSERVE", bizStep: MES_BIZSTEP.storing, disposition: disp, epcList: [outEpc], quantityList: [{ epcClass: outGtin, quantity: 1 }], readPoint: fgStore.id, bizLocation: fgStore.id }));
|
|
3049
4326
|
order.allocated = [];
|
|
3050
4327
|
order.fulfilled = 1;
|
|
@@ -3052,6 +4329,43 @@ var MesKernel = class extends FlowEngine {
|
|
|
3052
4329
|
this.emitOrder(order);
|
|
3053
4330
|
}
|
|
3054
4331
|
};
|
|
4332
|
+
|
|
4333
|
+
// src/vocabulary.ts
|
|
4334
|
+
var RETIRED_VOCABULARY = ["mover", "Mover", "MOVER", "node", "Node", "NODE"];
|
|
4335
|
+
var VOCABULARY_EXCEPTIONS = [
|
|
4336
|
+
/* ── 저널 와이어 필드 — append-only 역사이므로 이름을 바꾸지 않는다 ───────
|
|
4337
|
+
* 같은 사실이 시점에 따라 다른 키로 들어가면 낡은 이름보다 나쁘다. 개명은 이벤트 스키마
|
|
4338
|
+
* 버저닝과 함께 다룬다. (개명 시점 실측: moverId 8,965행 · fromNode/toNode 9,679행) */
|
|
4339
|
+
{ token: "moverId", why: "journal wire field \u2014 renaming would mix two keys for one fact across history" },
|
|
4340
|
+
{ token: "fromNode", why: "journal wire field (task.status payload)" },
|
|
4341
|
+
{ token: "toNode", why: "journal wire field (task.status payload)" },
|
|
4342
|
+
/* ── 씬 컴포넌트 타입 — 보드에 저장된 값이고, 뜻이 어긋나지도 않는다 ──────
|
|
4343
|
+
* 보드 7개가 이 타입으로 컴포넌트를 담고 있어 개명하면 그 컴포넌트가 조용히 안 그려진다.
|
|
4344
|
+
* 그리고 씬에서 이 이름은 자원이 아니라 **움직임 표현**을 가리킨다(표준과 충돌 아님). */
|
|
4345
|
+
{ token: "twin-mover", why: "scene component type persisted in boards; names a motion representation, not a resource" },
|
|
4346
|
+
{ token: "TwinMover", why: "class backing the persisted scene component type above" },
|
|
4347
|
+
/* ── 플랫폼·언어 어휘 — 우리 도메인 낱말이 아니다 ─────────────────────── */
|
|
4348
|
+
{ token: "node_modules", why: "npm path" },
|
|
4349
|
+
{ token: "node:", why: "Node.js builtin module specifier" },
|
|
4350
|
+
{ token: "nodemon", why: "tool name" },
|
|
4351
|
+
{ token: "Node.js", why: "platform name" },
|
|
4352
|
+
{ token: "NodeJS", why: "TypeScript global namespace" },
|
|
4353
|
+
{ token: "nodeName", why: "DOM API" },
|
|
4354
|
+
{ token: "childNodes", why: "DOM API" },
|
|
4355
|
+
{ token: "appendChild", why: "DOM API (no match, kept for clarity)" }
|
|
4356
|
+
];
|
|
4357
|
+
var GUARD_PRAGMA = "vocabulary-guard: allow";
|
|
4358
|
+
function retiredVocabularyIn(line) {
|
|
4359
|
+
if (line.includes(GUARD_PRAGMA)) return [];
|
|
4360
|
+
let rest = line;
|
|
4361
|
+
for (const { token } of VOCABULARY_EXCEPTIONS) rest = rest.split(token).join(" ");
|
|
4362
|
+
const hits = [];
|
|
4363
|
+
for (const word of RETIRED_VOCABULARY) {
|
|
4364
|
+
const re = new RegExp(`[A-Za-z_]*${word}[A-Za-z_]*`, "g");
|
|
4365
|
+
for (const m of rest.matchAll(re)) hits.push(m[0]);
|
|
4366
|
+
}
|
|
4367
|
+
return hits;
|
|
4368
|
+
}
|
|
3055
4369
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3056
4370
|
0 && (module.exports = {
|
|
3057
4371
|
BIZSTEP,
|
|
@@ -3060,65 +4374,99 @@ var MesKernel = class extends FlowEngine {
|
|
|
3060
4374
|
BTT_PRODORDER,
|
|
3061
4375
|
CAPABILITIES,
|
|
3062
4376
|
CAPABILITY_KEYS,
|
|
4377
|
+
CBV_BIZSTEP,
|
|
3063
4378
|
CMD,
|
|
3064
4379
|
DISP,
|
|
3065
4380
|
DOMAIN_CATALOG,
|
|
3066
4381
|
DOMAIN_SYSTEMS,
|
|
3067
4382
|
EPCIS_CONTEXT,
|
|
4383
|
+
EQUIPMENT_LEVEL,
|
|
3068
4384
|
EventJournal,
|
|
3069
4385
|
FlowEngine,
|
|
4386
|
+
GUARD_PRAGMA,
|
|
3070
4387
|
ILMD_ATTR,
|
|
4388
|
+
LOCATION_SATURATION_NEAR,
|
|
4389
|
+
MATERIAL_PROPERTY,
|
|
3071
4390
|
MES_BIZSTEP,
|
|
3072
|
-
|
|
4391
|
+
MES_LOCATION_TYPES,
|
|
3073
4392
|
MES_PART_GTINS,
|
|
3074
4393
|
MES_PRODUCTS,
|
|
3075
4394
|
MES_PRODUCT_GTINS,
|
|
3076
4395
|
MES_TYPES,
|
|
3077
4396
|
MesKernel,
|
|
3078
|
-
NODE_SATURATION_NEAR,
|
|
3079
4397
|
OP_EVENT,
|
|
3080
4398
|
OP_PARAM,
|
|
3081
4399
|
ObservedReducer,
|
|
4400
|
+
PRIORITY_UNSET,
|
|
4401
|
+
RETIRED_VOCABULARY,
|
|
3082
4402
|
StateProjector,
|
|
3083
4403
|
TwinHistory,
|
|
3084
4404
|
TwinObserver,
|
|
3085
4405
|
TwinRuntime,
|
|
3086
4406
|
UTC_OFFSET,
|
|
3087
|
-
|
|
4407
|
+
VOCABULARY_EXCEPTIONS,
|
|
4408
|
+
WMS_LOCATION_TYPES,
|
|
3088
4409
|
WMS_TYPES,
|
|
3089
4410
|
WmsKernel,
|
|
3090
4411
|
YARD_BIZSTEP,
|
|
3091
|
-
|
|
4412
|
+
YMS_LOCATION_TYPES,
|
|
3092
4413
|
YMS_TYPES,
|
|
3093
4414
|
YmsKernel,
|
|
4415
|
+
activeShiftAt,
|
|
4416
|
+
activeShiftOf,
|
|
3094
4417
|
aggregationEvent,
|
|
4418
|
+
analyzeCapacity,
|
|
3095
4419
|
capabilitiesForType,
|
|
4420
|
+
classClosure,
|
|
3096
4421
|
compareStates,
|
|
3097
4422
|
computeOee,
|
|
3098
4423
|
constantDuration,
|
|
4424
|
+
conversionFactorOf,
|
|
3099
4425
|
counterfactualAt,
|
|
3100
4426
|
deriveAttentions,
|
|
4427
|
+
dueStatusOf,
|
|
4428
|
+
effectivityAt,
|
|
3101
4429
|
fefoPolicy,
|
|
3102
4430
|
firstFitPolicy,
|
|
4431
|
+
foldJobResponses,
|
|
3103
4432
|
foldTaskRecords,
|
|
3104
4433
|
gdtiUri,
|
|
3105
4434
|
graiUri,
|
|
4435
|
+
hierarchyOf,
|
|
4436
|
+
inWorkCalendar,
|
|
4437
|
+
inWorkCalendarAt,
|
|
3106
4438
|
ingest,
|
|
4439
|
+
isEquipmentLevel,
|
|
4440
|
+
isoDurationHours,
|
|
4441
|
+
itemKeyOf,
|
|
3107
4442
|
lgtinClass,
|
|
4443
|
+
locationStatusOf,
|
|
3108
4444
|
mapRecord,
|
|
4445
|
+
minuteOfDayAt,
|
|
3109
4446
|
monteCarloForecast,
|
|
3110
|
-
nodeStatusOf,
|
|
3111
4447
|
objectEvent,
|
|
4448
|
+
offCalendarAt,
|
|
4449
|
+
offCalendarReasonAt,
|
|
3112
4450
|
parseEpc,
|
|
3113
4451
|
parseIsoDuration,
|
|
3114
4452
|
partialFitPolicy,
|
|
4453
|
+
priorityRank,
|
|
4454
|
+
quantityIn,
|
|
4455
|
+
readBoardAssets,
|
|
4456
|
+
readBoardEquipment,
|
|
4457
|
+
readBoardLocations,
|
|
3115
4458
|
replay,
|
|
4459
|
+
replaySegments,
|
|
4460
|
+
retiredVocabularyIn,
|
|
3116
4461
|
sgtinClass,
|
|
3117
4462
|
sgtinUri,
|
|
3118
4463
|
ssccUri,
|
|
3119
4464
|
stateFieldsOf,
|
|
4465
|
+
subLotIdOf,
|
|
3120
4466
|
transactionEvent,
|
|
3121
4467
|
transformationEvent,
|
|
3122
4468
|
validateDomainDefinition,
|
|
3123
|
-
validateEpcisEvent
|
|
4469
|
+
validateEpcisEvent,
|
|
4470
|
+
weekdayAt,
|
|
4471
|
+
workingTimeOfWeek
|
|
3124
4472
|
});
|