@operato/twin-kernel 0.2.2 → 0.3.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/contract.d.ts +336 -34
- package/dist/contract.js +199 -7
- package/dist/divergence.d.ts +1 -1
- package/dist/divergence.js +3 -3
- package/dist/domain-catalog.d.ts +4 -4
- package/dist/domain-catalog.js +4 -4
- package/dist/domain-definition.d.ts +5 -5
- package/dist/domain-definition.js +6 -6
- package/dist/event-journal.d.ts +1 -1
- package/dist/event-journal.js +1 -1
- package/dist/flow-engine.d.ts +55 -31
- package/dist/flow-engine.js +153 -124
- package/dist/forecast.d.ts +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/kernel.js +6 -6
- package/dist/mes-kernel.js +25 -25
- package/dist/mes-profile.d.ts +2 -2
- package/dist/mes-profile.js +11 -11
- package/dist/observed-reducer.d.ts +7 -7
- package/dist/observed-reducer.js +40 -30
- 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 +10 -10
- package/dist/yms-profile.d.ts +2 -2
- package/dist/yms-profile.js +6 -6
- package/dist-cjs/index.cjs +413 -214
- package/package.json +1 -1
package/dist-cjs/index.cjs
CHANGED
|
@@ -30,34 +30,39 @@ __export(index_exports, {
|
|
|
30
30
|
DOMAIN_CATALOG: () => DOMAIN_CATALOG,
|
|
31
31
|
DOMAIN_SYSTEMS: () => DOMAIN_SYSTEMS,
|
|
32
32
|
EPCIS_CONTEXT: () => EPCIS_CONTEXT,
|
|
33
|
+
EQUIPMENT_LEVEL: () => EQUIPMENT_LEVEL,
|
|
33
34
|
EventJournal: () => EventJournal,
|
|
34
35
|
FlowEngine: () => FlowEngine,
|
|
36
|
+
GUARD_PRAGMA: () => GUARD_PRAGMA,
|
|
35
37
|
ILMD_ATTR: () => ILMD_ATTR,
|
|
38
|
+
LOCATION_SATURATION_NEAR: () => LOCATION_SATURATION_NEAR,
|
|
36
39
|
MES_BIZSTEP: () => MES_BIZSTEP,
|
|
37
|
-
|
|
40
|
+
MES_LOCATION_TYPES: () => MES_LOCATION_TYPES,
|
|
38
41
|
MES_PART_GTINS: () => MES_PART_GTINS,
|
|
39
42
|
MES_PRODUCTS: () => MES_PRODUCTS,
|
|
40
43
|
MES_PRODUCT_GTINS: () => MES_PRODUCT_GTINS,
|
|
41
44
|
MES_TYPES: () => MES_TYPES,
|
|
42
45
|
MesKernel: () => MesKernel,
|
|
43
|
-
NODE_SATURATION_NEAR: () => NODE_SATURATION_NEAR,
|
|
44
46
|
OP_EVENT: () => OP_EVENT,
|
|
45
47
|
OP_PARAM: () => OP_PARAM,
|
|
46
48
|
ObservedReducer: () => ObservedReducer,
|
|
49
|
+
RETIRED_VOCABULARY: () => RETIRED_VOCABULARY,
|
|
47
50
|
StateProjector: () => ObservedReducer,
|
|
48
51
|
TwinHistory: () => TwinHistory,
|
|
49
52
|
TwinObserver: () => TwinObserver,
|
|
50
53
|
TwinRuntime: () => TwinRuntime,
|
|
51
54
|
UTC_OFFSET: () => UTC_OFFSET,
|
|
52
|
-
|
|
55
|
+
VOCABULARY_EXCEPTIONS: () => VOCABULARY_EXCEPTIONS,
|
|
56
|
+
WMS_LOCATION_TYPES: () => WMS_LOCATION_TYPES,
|
|
53
57
|
WMS_TYPES: () => WMS_TYPES,
|
|
54
58
|
WmsKernel: () => WmsKernel,
|
|
55
59
|
YARD_BIZSTEP: () => YARD_BIZSTEP,
|
|
56
|
-
|
|
60
|
+
YMS_LOCATION_TYPES: () => YMS_LOCATION_TYPES,
|
|
57
61
|
YMS_TYPES: () => YMS_TYPES,
|
|
58
62
|
YmsKernel: () => YmsKernel,
|
|
59
63
|
aggregationEvent: () => aggregationEvent,
|
|
60
64
|
capabilitiesForType: () => capabilitiesForType,
|
|
65
|
+
classClosure: () => classClosure,
|
|
61
66
|
compareStates: () => compareStates,
|
|
62
67
|
computeOee: () => computeOee,
|
|
63
68
|
constantDuration: () => constantDuration,
|
|
@@ -68,16 +73,22 @@ __export(index_exports, {
|
|
|
68
73
|
foldTaskRecords: () => foldTaskRecords,
|
|
69
74
|
gdtiUri: () => gdtiUri,
|
|
70
75
|
graiUri: () => graiUri,
|
|
76
|
+
hierarchyOf: () => hierarchyOf,
|
|
71
77
|
ingest: () => ingest,
|
|
78
|
+
isEquipmentLevel: () => isEquipmentLevel,
|
|
72
79
|
lgtinClass: () => lgtinClass,
|
|
80
|
+
locationStatusOf: () => locationStatusOf,
|
|
73
81
|
mapRecord: () => mapRecord,
|
|
74
82
|
monteCarloForecast: () => monteCarloForecast,
|
|
75
|
-
nodeStatusOf: () => nodeStatusOf,
|
|
76
83
|
objectEvent: () => objectEvent,
|
|
77
84
|
parseEpc: () => parseEpc,
|
|
78
85
|
parseIsoDuration: () => parseIsoDuration,
|
|
79
86
|
partialFitPolicy: () => partialFitPolicy,
|
|
87
|
+
readBoardAssets: () => readBoardAssets,
|
|
88
|
+
readBoardEquipment: () => readBoardEquipment,
|
|
89
|
+
readBoardLocations: () => readBoardLocations,
|
|
80
90
|
replay: () => replay,
|
|
91
|
+
retiredVocabularyIn: () => retiredVocabularyIn,
|
|
81
92
|
sgtinClass: () => sgtinClass,
|
|
82
93
|
sgtinUri: () => sgtinUri,
|
|
83
94
|
ssccUri: () => ssccUri,
|
|
@@ -90,12 +101,112 @@ __export(index_exports, {
|
|
|
90
101
|
module.exports = __toCommonJS(index_exports);
|
|
91
102
|
|
|
92
103
|
// src/contract.ts
|
|
93
|
-
var
|
|
94
|
-
function
|
|
104
|
+
var LOCATION_SATURATION_NEAR = 0.9;
|
|
105
|
+
function locationStatusOf(n) {
|
|
95
106
|
const cap = n.capacity;
|
|
96
107
|
if (!(typeof cap === "number" && cap > 0)) return void 0;
|
|
97
108
|
const r = (n.occupancy ?? 0) / cap;
|
|
98
|
-
return r >= 1 ? "full" : r >=
|
|
109
|
+
return r >= 1 ? "full" : r >= LOCATION_SATURATION_NEAR ? "near-full" : "available";
|
|
110
|
+
}
|
|
111
|
+
var EQUIPMENT_LEVEL = [
|
|
112
|
+
"Enterprise",
|
|
113
|
+
"Site",
|
|
114
|
+
"Area",
|
|
115
|
+
"ProcessCell",
|
|
116
|
+
"Unit",
|
|
117
|
+
"ProductionLine",
|
|
118
|
+
"WorkCell",
|
|
119
|
+
"ProductionUnit",
|
|
120
|
+
"StorageZone",
|
|
121
|
+
"StorageUnit",
|
|
122
|
+
"WorkCenter",
|
|
123
|
+
"WorkUnit",
|
|
124
|
+
"EquipmentModule",
|
|
125
|
+
"ControlModule",
|
|
126
|
+
"Other"
|
|
127
|
+
];
|
|
128
|
+
function isEquipmentLevel(v) {
|
|
129
|
+
return typeof v === "string" && EQUIPMENT_LEVEL.includes(v);
|
|
130
|
+
}
|
|
131
|
+
function hierarchyOf(s) {
|
|
132
|
+
const push = (m, k, v) => {
|
|
133
|
+
const cur = m.get(k);
|
|
134
|
+
if (cur) cur.push(v);
|
|
135
|
+
else m.set(k, [v]);
|
|
136
|
+
};
|
|
137
|
+
const parent = /* @__PURE__ */ new Map();
|
|
138
|
+
const kids = /* @__PURE__ */ new Map();
|
|
139
|
+
const known = new Set(s.locations.map((n) => n.id));
|
|
140
|
+
for (const n of s.locations) {
|
|
141
|
+
if (!n.parentId || n.parentId === n.id) continue;
|
|
142
|
+
parent.set(n.id, n.parentId);
|
|
143
|
+
if (known.has(n.parentId)) push(kids, n.parentId, n.id);
|
|
144
|
+
}
|
|
145
|
+
for (const start of known) {
|
|
146
|
+
const seen = [start];
|
|
147
|
+
for (let at = parent.get(start); at !== void 0; at = parent.get(at)) {
|
|
148
|
+
if (seen.includes(at)) throw new Error(`location hierarchy has a cycle: ${[...seen, at].join(" \u2192 ")}`);
|
|
149
|
+
seen.push(at);
|
|
150
|
+
if (!known.has(at)) break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const homes = /* @__PURE__ */ new Map();
|
|
154
|
+
for (const m of s.equipment ?? []) if (m.homeLocation) push(homes, m.homeLocation, m.id);
|
|
155
|
+
const ancestorsOf = (id) => {
|
|
156
|
+
const out = [];
|
|
157
|
+
for (let at = parent.get(id); at !== void 0; at = parent.get(at)) {
|
|
158
|
+
out.push(at);
|
|
159
|
+
if (!known.has(at)) break;
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
};
|
|
163
|
+
const descendantsOf = (id) => {
|
|
164
|
+
const out = [];
|
|
165
|
+
const stack = [...kids.get(id) ?? []];
|
|
166
|
+
while (stack.length) {
|
|
167
|
+
const cur = stack.pop();
|
|
168
|
+
out.push(cur);
|
|
169
|
+
stack.push(...kids.get(cur) ?? []);
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
};
|
|
173
|
+
const typeOf = new Map(s.locations.map((n) => [n.id, n.type]));
|
|
174
|
+
const levelOf = new Map(s.locations.map((n) => [n.id, n.level]));
|
|
175
|
+
return {
|
|
176
|
+
childrenOf: (id) => [...kids.get(id) ?? []],
|
|
177
|
+
ancestorsOf,
|
|
178
|
+
descendantsOf,
|
|
179
|
+
rollupOf: (id) => ancestorsOf(id).find((a) => !known.has(a)),
|
|
180
|
+
ancestorOfLevel: (id, level) => ancestorsOf(id).find((a) => known.has(a) && levelOf.get(a) === level),
|
|
181
|
+
ancestorOfType: (id, type) => ancestorsOf(id).find((a) => known.has(a) && typeOf.get(a) === type),
|
|
182
|
+
equipmentOf: (id, opts) => {
|
|
183
|
+
const scope = opts?.deep ? [id, ...descendantsOf(id)] : [id];
|
|
184
|
+
return scope.flatMap((n) => homes.get(n) ?? []);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function classClosure(directIds, defs, at) {
|
|
189
|
+
const byId = new Map((defs ?? []).map((d) => [d.id, d]));
|
|
190
|
+
const atMs = at ? Date.parse(at) : NaN;
|
|
191
|
+
const inWindow = (d) => {
|
|
192
|
+
if (!d || !Number.isFinite(atMs)) return true;
|
|
193
|
+
const from = d.effectiveStart ? Date.parse(d.effectiveStart) : NaN;
|
|
194
|
+
const to = d.effectiveEnd ? Date.parse(d.effectiveEnd) : NaN;
|
|
195
|
+
if (Number.isFinite(from) && atMs < from) return false;
|
|
196
|
+
if (Number.isFinite(to) && atMs > to) return false;
|
|
197
|
+
return true;
|
|
198
|
+
};
|
|
199
|
+
const out = /* @__PURE__ */ new Set();
|
|
200
|
+
const stack = [...directIds ?? []];
|
|
201
|
+
while (stack.length) {
|
|
202
|
+
const id = stack.pop();
|
|
203
|
+
if (out.has(id)) continue;
|
|
204
|
+
const def = byId.get(id);
|
|
205
|
+
if (!inWindow(def)) continue;
|
|
206
|
+
out.add(id);
|
|
207
|
+
for (const b of def?.baseIds ?? []) if (!out.has(b)) stack.push(b);
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
99
210
|
}
|
|
100
211
|
var OP_EVENT = {
|
|
101
212
|
task: "task.status",
|
|
@@ -114,7 +225,7 @@ var CMD = {
|
|
|
114
225
|
orderRelease: "order.release",
|
|
115
226
|
attentionAck: "attention.ack",
|
|
116
227
|
// 주목 신호 확인(OPC UA A&C acknowledge) — args:{id}
|
|
117
|
-
// Operable 코어 — 모든 operable 자원(
|
|
228
|
+
// Operable 코어 — 모든 operable 자원(설비·이동설비) 공통. args:{resourceId}. capability-keyed(무방언, equipment.* 아님).
|
|
118
229
|
resourceHold: "resource.hold",
|
|
119
230
|
// 계획 정지(정비/오프라인) — 배정 스킵
|
|
120
231
|
resourceResume: "resource.resume",
|
|
@@ -126,8 +237,27 @@ var CMD = {
|
|
|
126
237
|
resourceResetMetrics: "resource.reset-metrics",
|
|
127
238
|
// OEE 계측 창 리셋
|
|
128
239
|
resourceAdd: "resource.add"
|
|
129
|
-
// 라이브 자원(
|
|
240
|
+
// 라이브 자원(설비) 추가 — args:{kind, homeLocation, count?}. 런타임 구조 변이(what-if 아닌 실제 act)
|
|
130
241
|
};
|
|
242
|
+
function readBoardLocations(def) {
|
|
243
|
+
const d = def;
|
|
244
|
+
return d.locations ?? d.nodes ?? [];
|
|
245
|
+
}
|
|
246
|
+
function readBoardEquipment(def) {
|
|
247
|
+
const d = def;
|
|
248
|
+
const list = d.equipment ?? d.equipmentList ?? [];
|
|
249
|
+
return list.map((e) => normalizeHomeLocation(e));
|
|
250
|
+
}
|
|
251
|
+
function normalizeHomeLocation(entry) {
|
|
252
|
+
const legacy = entry.homeNode;
|
|
253
|
+
if (legacy === void 0 || entry.homeLocation !== void 0) return entry;
|
|
254
|
+
const { homeNode: _drop, ...rest } = entry;
|
|
255
|
+
return { ...rest, homeLocation: legacy };
|
|
256
|
+
}
|
|
257
|
+
function readBoardAssets(def) {
|
|
258
|
+
const list = def.assets ?? [];
|
|
259
|
+
return list.map((a) => normalizeHomeLocation(a));
|
|
260
|
+
}
|
|
131
261
|
|
|
132
262
|
// src/domain-definition.ts
|
|
133
263
|
var OP_PARAM = {
|
|
@@ -152,19 +282,19 @@ function validateDomainDefinition(def) {
|
|
|
152
282
|
if (!def || typeof def !== "object") return ["domain definition \uC5C6\uC74C/\uAC1D\uCCB4 \uC544\uB2D8"];
|
|
153
283
|
if (typeof def.id !== "string" || !def.id) v.push("id \uB204\uB77D");
|
|
154
284
|
if (typeof def.label !== "string" || !def.label) v.push("label \uB204\uB77D");
|
|
155
|
-
if (!Array.isArray(def.
|
|
285
|
+
if (!Array.isArray(def.locationTypes) || def.locationTypes.length === 0) v.push("locationTypes \uBE44\uC5B4\uC788\uC74C");
|
|
156
286
|
if (!Array.isArray(def.resourceTypes)) v.push("resourceTypes \uBC30\uC5F4 \uC544\uB2D8");
|
|
157
|
-
const
|
|
287
|
+
const locationKeys2 = new Set((def.locationTypes || []).map((n) => n.key));
|
|
158
288
|
const resKeys = new Set((def.resourceTypes || []).map((r) => r.key));
|
|
159
289
|
const matKeys = new Set((def.materials || []).map((m) => m.key));
|
|
160
290
|
const opKeys = new Set((def.operations || []).map((o) => o.key));
|
|
161
291
|
const routeKeys = new Set((def.routes || []).map((r) => r.key));
|
|
162
|
-
for (const [name, arr] of [["
|
|
292
|
+
for (const [name, arr] of [["locationTypes", def.locationTypes], ["resourceTypes", def.resourceTypes], ["materials", def.materials], ["operations", def.operations], ["routes", def.routes], ["recipes", def.recipes]]) {
|
|
163
293
|
for (const d of dupes((arr || []).map((x) => x.key))) v.push(`${name} \uD0A4 \uC911\uBCF5: ${d}`);
|
|
164
294
|
}
|
|
165
295
|
for (const o of def.operations || []) {
|
|
166
296
|
if (!INTENTS.includes(o.intent)) v.push(`operation '${o.key}' intent \uBD80\uC815: ${o.intent}`);
|
|
167
|
-
if (o.
|
|
297
|
+
if (o.locationType && !locationKeys2.has(o.locationType)) v.push(`operation '${o.key}' locationType '${o.locationType}' \uBBF8\uC815\uC758`);
|
|
168
298
|
if (o.resourceType && !resKeys.has(o.resourceType)) v.push(`operation '${o.key}' resourceType '${o.resourceType}' \uBBF8\uC815\uC758`);
|
|
169
299
|
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
300
|
}
|
|
@@ -198,10 +328,10 @@ function diffBy(predicted, actual, idOf, valOf) {
|
|
|
198
328
|
function compareStates(predicted, actual) {
|
|
199
329
|
const itemLocation = diffBy(predicted.items, actual.items, (i) => i.epc, (i) => i.location);
|
|
200
330
|
const itemDisposition = diffBy(predicted.items, actual.items, (i) => i.epc, (i) => i.disposition);
|
|
201
|
-
const
|
|
331
|
+
const locationOccupancy = diffBy(predicted.locations, actual.locations, (n) => n.id, (n) => n.occupancy);
|
|
202
332
|
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,
|
|
333
|
+
const hasDrift = itemLocation.length > 0 || itemDisposition.length > 0 || locationOccupancy.length > 0 || orderStatus.length > 0;
|
|
334
|
+
return { hasDrift, itemLocation, itemDisposition, locationOccupancy, orderStatus };
|
|
205
335
|
}
|
|
206
336
|
|
|
207
337
|
// src/counterfactual.ts
|
|
@@ -538,7 +668,7 @@ var ObservedReducer = class {
|
|
|
538
668
|
pendingParent = /* @__PURE__ */ new Map();
|
|
539
669
|
// 자식 EPC → 부모(물류단위)
|
|
540
670
|
tasks = /* @__PURE__ */ new Map();
|
|
541
|
-
|
|
671
|
+
equipment = /* @__PURE__ */ new Map();
|
|
542
672
|
persons = /* @__PURE__ */ new Map();
|
|
543
673
|
assets = /* @__PURE__ */ new Map();
|
|
544
674
|
orders = /* @__PURE__ */ new Map();
|
|
@@ -546,26 +676,26 @@ var ObservedReducer = class {
|
|
|
546
676
|
/** 받은 정정 선언 — 상태에 반영하지 않되 **버리지도 않는다**(소비처가 볼 수 있게). */
|
|
547
677
|
corrections = [];
|
|
548
678
|
constructor(board) {
|
|
549
|
-
for (const n of board
|
|
550
|
-
for (const m of board
|
|
551
|
-
for (const p of board.persons ?? []) this.persons.set(p.id, { id: p.id,
|
|
552
|
-
for (const a of board
|
|
679
|
+
for (const n of readBoardLocations(board)) this.master.set(n.id, { id: n.id, type: n.type, capacity: n.capacity, parallelism: n.parallelism, parentId: n.parentId, origin: "master" });
|
|
680
|
+
for (const m of readBoardEquipment(board)) this.equipment.set(m.id, { id: m.id, kind: m.kind, status: "idle", location: m.homeLocation, homeLocation: m.homeLocation, ...m.properties ? { properties: m.properties } : {}, ...m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}, origin: "master" });
|
|
681
|
+
for (const p of board.persons ?? []) this.persons.set(p.id, { id: p.id, personnelClassIds: p.personnelClassIds, status: "idle", ...p.homeLocation ? { location: p.homeLocation } : {}, ...p.properties ? { properties: p.properties } : {}, ...p.testSpecificationIds ? { testSpecificationIds: p.testSpecificationIds } : {} });
|
|
682
|
+
for (const 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 } : {} });
|
|
553
683
|
}
|
|
554
684
|
/** 마스터 동기 — 로케이션 추가/변경/제거. */
|
|
555
685
|
applyMaster(u) {
|
|
556
686
|
if (u.op === "remove") {
|
|
557
|
-
this.master.delete(u.
|
|
687
|
+
this.master.delete(u.location.id);
|
|
558
688
|
return;
|
|
559
689
|
}
|
|
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.
|
|
690
|
+
const cur = this.master.get(u.location.id);
|
|
691
|
+
const capacity = u.location.capacity ?? cur?.capacity;
|
|
692
|
+
const parallelism = u.location.parallelism ?? cur?.parallelism;
|
|
693
|
+
this.master.set(u.location.id, {
|
|
694
|
+
id: u.location.id,
|
|
695
|
+
type: u.location.type ?? cur?.type ?? UNKNOWN_TYPE,
|
|
566
696
|
...capacity === void 0 ? {} : { capacity },
|
|
567
697
|
...parallelism === void 0 ? {} : { parallelism },
|
|
568
|
-
...u.
|
|
698
|
+
...u.location.parentId ?? cur?.parentId ? { parentId: u.location.parentId ?? cur?.parentId } : {},
|
|
569
699
|
/* 마스터가 말한 것은 마스터 출처다 — 관측으로 알게 된 것(origin='observed')을 덮어 승격한다. */
|
|
570
700
|
origin: "master"
|
|
571
701
|
});
|
|
@@ -573,7 +703,7 @@ var ObservedReducer = class {
|
|
|
573
703
|
/**
|
|
574
704
|
* 관측된 로케이션을 구조로 승격 — **이벤트가 가르쳐 준 것을 구조에서 지우지 않는다.**
|
|
575
705
|
*
|
|
576
|
-
* 마스터에 없는 로케이션에서 물품이 관측되면, 예전에는 물품의 `location` 에만 남고 `
|
|
706
|
+
* 마스터에 없는 로케이션에서 물품이 관측되면, 예전에는 물품의 `location` 에만 남고 `locations` 에는
|
|
577
707
|
* 나타나지 않았다. 그 결과 그 자리는 스키매틱에 없고, 점유가 집계되지 않고, 병목 주목이 뜰 수
|
|
578
708
|
* 없었다 — **사실은 들어왔는데 구조가 모르는 상태.** 이제 최소 형태로 승격한다:
|
|
579
709
|
* 종류는 모르므로 `unknown`, **용량은 비워 둔다**(발명하지 않는다), 출처는 `observed`.
|
|
@@ -646,20 +776,25 @@ var ObservedReducer = class {
|
|
|
646
776
|
}
|
|
647
777
|
case OP_EVENT.equipment: {
|
|
648
778
|
const d = e.data;
|
|
649
|
-
if (this.stale(`
|
|
779
|
+
if (this.stale(`eq:${d.moverId}`, e)) return;
|
|
650
780
|
this.touchLocation(d.location);
|
|
651
|
-
const known = this.
|
|
652
|
-
|
|
781
|
+
const known = this.equipment.get(d.moverId);
|
|
782
|
+
const homeLocation = d.homeLocation ?? known?.homeLocation;
|
|
783
|
+
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" });
|
|
653
784
|
break;
|
|
654
785
|
}
|
|
655
786
|
case OP_EVENT.person: {
|
|
656
787
|
const d = e.data;
|
|
657
788
|
if (this.stale(`person:${d.personId}`, e)) return;
|
|
789
|
+
const known = this.persons.get(d.personId);
|
|
658
790
|
this.persons.set(d.personId, {
|
|
659
791
|
id: d.personId,
|
|
660
|
-
|
|
792
|
+
personnelClassIds: d.personnelClassIds ?? known?.personnelClassIds,
|
|
661
793
|
status: d.status,
|
|
662
794
|
taskId: d.taskId,
|
|
795
|
+
...d.location ?? known?.location ? { location: d.location ?? known?.location } : {},
|
|
796
|
+
...known?.properties ? { properties: known.properties } : {},
|
|
797
|
+
...known?.testSpecificationIds ? { testSpecificationIds: known.testSpecificationIds } : {},
|
|
663
798
|
...d.offShift ? { offShift: true } : {}
|
|
664
799
|
});
|
|
665
800
|
break;
|
|
@@ -670,7 +805,7 @@ var ObservedReducer = class {
|
|
|
670
805
|
const cur = this.assets.get(d.assetId);
|
|
671
806
|
this.assets.set(d.assetId, {
|
|
672
807
|
id: d.assetId,
|
|
673
|
-
|
|
808
|
+
assetClassIds: d.assetClassIds ?? cur?.assetClassIds,
|
|
674
809
|
location: d.location ?? cur?.location,
|
|
675
810
|
status: d.status,
|
|
676
811
|
taskId: d.taskId,
|
|
@@ -803,16 +938,16 @@ var ObservedReducer = class {
|
|
|
803
938
|
for (const c of children) this.remove(c);
|
|
804
939
|
}
|
|
805
940
|
}
|
|
806
|
-
/** 현재 투영 State —
|
|
941
|
+
/** 현재 투영 State — 자리 점유는 아이템 위치 집계로 유도(pure projection). */
|
|
807
942
|
snapshot() {
|
|
808
943
|
const occ = /* @__PURE__ */ new Map();
|
|
809
944
|
for (const it of this.items.values()) occ.set(it.location, (occ.get(it.location) ?? 0) + 1);
|
|
810
945
|
return {
|
|
811
946
|
revision: this.revision,
|
|
812
947
|
...this.corrections.length ? { corrections: this.corrections.map((c) => ({ ...c })) } : {},
|
|
813
|
-
|
|
948
|
+
locations: [...this.master.values()].map((n) => {
|
|
814
949
|
const occupancy = occ.get(n.id) ?? 0;
|
|
815
|
-
const status =
|
|
950
|
+
const status = locationStatusOf({ occupancy, capacity: n.capacity });
|
|
816
951
|
return {
|
|
817
952
|
id: n.id,
|
|
818
953
|
type: n.type,
|
|
@@ -830,7 +965,7 @@ var ObservedReducer = class {
|
|
|
830
965
|
persons: [...this.persons.values()].map((p) => ({ ...p })),
|
|
831
966
|
assets: [...this.assets.values()].map((a) => ({ ...a })),
|
|
832
967
|
tasks: [...this.tasks.values()].map((t) => ({ ...t })),
|
|
833
|
-
|
|
968
|
+
equipment: [...this.equipment.values()].map((m) => ({ ...m })),
|
|
834
969
|
orders: [...this.orders.values()].map((o) => ({ ...o }))
|
|
835
970
|
};
|
|
836
971
|
}
|
|
@@ -882,10 +1017,10 @@ var BTT = {
|
|
|
882
1017
|
po: "urn:epcglobal:cbv:btt:po",
|
|
883
1018
|
so: "urn:epcglobal:cbv:btt:so"
|
|
884
1019
|
};
|
|
885
|
-
var
|
|
1020
|
+
var WMS_LOCATION_TYPES = ["dock", "storage", "staging", "dock-ship"];
|
|
886
1021
|
var WMS_TYPES = [
|
|
887
|
-
...
|
|
888
|
-
{ key: "forklift", role: "
|
|
1022
|
+
...WMS_LOCATION_TYPES.map((k) => ({ key: k, role: "location", label: `twin.type.${k}`, standardClass: { epcis: "bizLocation" }, identity: { scheme: "gs1:SGLN" }, capabilities: ["storable"] })),
|
|
1023
|
+
{ key: "forklift", role: "equipment", label: "twin.type.forklift", standardClass: { epcis: "object", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["mobile", "operable"] }
|
|
889
1024
|
];
|
|
890
1025
|
|
|
891
1026
|
// src/capability.ts
|
|
@@ -908,7 +1043,7 @@ var CAPABILITIES = {
|
|
|
908
1043
|
mobile: {
|
|
909
1044
|
key: "mobile",
|
|
910
1045
|
label: "\uC774\uB3D9",
|
|
911
|
-
semantics: "\uC790\uC6D0 \uC790\uC2E0\uC774 \
|
|
1046
|
+
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
1047
|
stateFields: ["location", "motion"],
|
|
913
1048
|
models: ["Motion"],
|
|
914
1049
|
results: ["moved", "motionTick"]
|
|
@@ -952,10 +1087,10 @@ var BTT_DELIVERY = "urn:epcglobal:cbv:btt:deliv";
|
|
|
952
1087
|
function graiUri(companyPrefix, assetType, serial) {
|
|
953
1088
|
return `urn:epc:id:grai:${companyPrefix}.${assetType}.${String(serial).padStart(6, "0")}`;
|
|
954
1089
|
}
|
|
955
|
-
var
|
|
1090
|
+
var YMS_LOCATION_TYPES = ["gate", "yard-slot", "dock-door", "staging"];
|
|
956
1091
|
var YMS_TYPES = [
|
|
957
|
-
...
|
|
958
|
-
{ key: "hostler", role: "
|
|
1092
|
+
...YMS_LOCATION_TYPES.map((k) => ({ key: k, role: "location", label: `twin.type.${k}`, standardClass: { epcis: "bizLocation" }, identity: { scheme: "gs1:SGLN" }, capabilities: ["storable"] })),
|
|
1093
|
+
{ key: "hostler", role: "equipment", label: "twin.type.hostler", standardClass: { epcis: "object", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["mobile", "operable"] }
|
|
959
1094
|
];
|
|
960
1095
|
|
|
961
1096
|
// src/mes-profile.ts
|
|
@@ -971,8 +1106,8 @@ var BTT_PRODORDER = "urn:epcglobal:cbv:btt:prodorder";
|
|
|
971
1106
|
function sgtinUri(companyPrefix, itemRef, serial) {
|
|
972
1107
|
return `urn:epc:id:sgtin:${companyPrefix}.${itemRef}.${serial}`;
|
|
973
1108
|
}
|
|
974
|
-
var
|
|
975
|
-
var
|
|
1109
|
+
var MES_LOCATION_TYPES = ["raw-store", "cut-station", "weld-station", "paint-booth", "assembly-line", "fg-store"];
|
|
1110
|
+
var MES_LOCATION_CLS = {
|
|
976
1111
|
"raw-store": { epcis: "bizLocation" },
|
|
977
1112
|
"cut-station": { isa95: "WorkCenter", epcis: "bizLocation" },
|
|
978
1113
|
"weld-station": { isa95: "WorkCenter", epcis: "bizLocation" },
|
|
@@ -981,20 +1116,20 @@ var MES_NODE_CLS = {
|
|
|
981
1116
|
"fg-store": { epcis: "bizLocation" }
|
|
982
1117
|
};
|
|
983
1118
|
var MES_TYPES = [
|
|
984
|
-
...
|
|
985
|
-
{ key: "cutter", role: "
|
|
986
|
-
{ key: "welder", role: "
|
|
987
|
-
{ key: "painter", role: "
|
|
988
|
-
{ key: "assembler", role: "
|
|
1119
|
+
...MES_LOCATION_TYPES.map((k) => ({ key: k, role: "location", label: `twin.type.${k}`, standardClass: MES_LOCATION_CLS[k] ?? {}, identity: { scheme: "gs1:SGLN" }, capabilities: ["storable"] })),
|
|
1120
|
+
{ key: "cutter", role: "equipment", label: "twin.type.cutter", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] },
|
|
1121
|
+
{ key: "welder", role: "equipment", label: "twin.type.welder", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] },
|
|
1122
|
+
{ key: "painter", role: "equipment", label: "twin.type.painter", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] },
|
|
1123
|
+
{ key: "assembler", role: "equipment", label: "twin.type.assembler", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] }
|
|
989
1124
|
];
|
|
990
1125
|
|
|
991
1126
|
// src/domain-catalog.ts
|
|
992
|
-
var
|
|
1127
|
+
var locationKeys = (types) => types.filter((t) => t.role === "location").map((t) => t.key);
|
|
993
1128
|
var DOMAIN_CATALOG = {
|
|
994
1129
|
// 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,
|
|
1130
|
+
wms: { system: "wms", label: "twin.system.wms", types: WMS_TYPES, locationTypes: locationKeys(WMS_TYPES) },
|
|
1131
|
+
yms: { system: "yms", label: "twin.system.yms", types: YMS_TYPES, locationTypes: locationKeys(YMS_TYPES) },
|
|
1132
|
+
mes: { system: "mes", label: "twin.system.mes", types: MES_TYPES, locationTypes: locationKeys(MES_TYPES) }
|
|
998
1133
|
};
|
|
999
1134
|
var DOMAIN_SYSTEMS = ["wms", "yms", "mes"];
|
|
1000
1135
|
function capabilitiesForType(system, typeKey) {
|
|
@@ -1083,8 +1218,8 @@ function foldTaskRecords(rows) {
|
|
|
1083
1218
|
if (f.resource === void 0 && d.resourceRef) f.resource = d.resourceRef;
|
|
1084
1219
|
if (f.kind === void 0 && d.kind) f.kind = d.kind;
|
|
1085
1220
|
if (f.order === void 0 && d.orderId) f.order = d.orderId;
|
|
1086
|
-
const
|
|
1087
|
-
if (
|
|
1221
|
+
const loc = d.toNode ?? d.fromNode;
|
|
1222
|
+
if (loc) f.location = loc;
|
|
1088
1223
|
if (d.status === "created") rec.createdMs = at;
|
|
1089
1224
|
else if (d.status === "in-progress") rec.startedMs = rec.startedMs != null ? Math.min(rec.startedMs, at) : at;
|
|
1090
1225
|
else if (d.status === "completed") rec.completedMs = rec.completedMs != null ? Math.max(rec.completedMs, at) : at;
|
|
@@ -1209,14 +1344,14 @@ function mulberry32(seed) {
|
|
|
1209
1344
|
}
|
|
1210
1345
|
function deriveAttentions(view, acked) {
|
|
1211
1346
|
const out = [];
|
|
1212
|
-
for (const m of view.
|
|
1347
|
+
for (const m of view.equipment) {
|
|
1213
1348
|
if (m.status === "down") {
|
|
1214
1349
|
out.push({
|
|
1215
1350
|
id: `breakdown:${m.id}`,
|
|
1216
1351
|
kind: "breakdown",
|
|
1217
1352
|
severity: "critical",
|
|
1218
|
-
anchor: { moverId: m.id,
|
|
1219
|
-
params: { moverId: m.id, ...m.location ? {
|
|
1353
|
+
anchor: { moverId: m.id, locationId: m.location },
|
|
1354
|
+
params: { moverId: m.id, ...m.location ? { locationId: m.location } : {} },
|
|
1220
1355
|
recommendedActions: [
|
|
1221
1356
|
{ code: "act.repair", command: CMD.resourceRepair, args: { resourceId: m.id } },
|
|
1222
1357
|
{ code: "act.hold-until-repair", command: CMD.resourceHold, args: { resourceId: m.id } }
|
|
@@ -1225,7 +1360,7 @@ function deriveAttentions(view, acked) {
|
|
|
1225
1360
|
});
|
|
1226
1361
|
}
|
|
1227
1362
|
}
|
|
1228
|
-
for (const n of view.
|
|
1363
|
+
for (const n of view.locations) {
|
|
1229
1364
|
if ((n.capacity ?? 0) > 0) {
|
|
1230
1365
|
const r = (n.occupancy ?? 0) / n.capacity;
|
|
1231
1366
|
if (r >= 0.9) {
|
|
@@ -1234,15 +1369,15 @@ function deriveAttentions(view, acked) {
|
|
|
1234
1369
|
id: `bottleneck:${n.id}`,
|
|
1235
1370
|
kind: "bottleneck",
|
|
1236
1371
|
severity: saturated ? "high" : "medium",
|
|
1237
|
-
anchor: {
|
|
1238
|
-
params: {
|
|
1372
|
+
anchor: { locationId: n.id },
|
|
1373
|
+
params: { locationId: n.id, occupancy: n.occupancy ?? 0, capacity: n.capacity ?? 0, ratioPct: Math.round(r * 100), saturated: saturated ? 1 : 0 },
|
|
1239
1374
|
recommendedActions: [{ code: "advice.add-resource" }, { code: "advice.downstream-priority" }]
|
|
1240
1375
|
// 권고만(command 없음) — 표현계층이 advice.* 코드로 렌더
|
|
1241
1376
|
});
|
|
1242
1377
|
}
|
|
1243
1378
|
}
|
|
1244
1379
|
}
|
|
1245
|
-
for (const m of view.
|
|
1380
|
+
for (const m of view.equipment) {
|
|
1246
1381
|
const total = (m.goodCount ?? 0) + (m.scrapCount ?? 0);
|
|
1247
1382
|
if (total >= 10) {
|
|
1248
1383
|
const rate = (m.scrapCount ?? 0) / total;
|
|
@@ -1250,7 +1385,7 @@ function deriveAttentions(view, acked) {
|
|
|
1250
1385
|
id: `scrap:${m.id}`,
|
|
1251
1386
|
kind: "scrap-high",
|
|
1252
1387
|
severity: rate >= 0.3 ? "high" : "medium",
|
|
1253
|
-
anchor: { moverId: m.id,
|
|
1388
|
+
anchor: { moverId: m.id, locationId: m.location },
|
|
1254
1389
|
params: { moverId: m.id, goodCount: m.goodCount ?? 0, scrapCount: m.scrapCount ?? 0, ratePct: Math.round(rate * 100) },
|
|
1255
1390
|
recommendedActions: [
|
|
1256
1391
|
{ code: "act.hold-for-inspection", command: CMD.resourceHold, args: { resourceId: m.id } },
|
|
@@ -1298,9 +1433,11 @@ function computeOee(c, nowMs) {
|
|
|
1298
1433
|
}
|
|
1299
1434
|
var FlowEngine = class {
|
|
1300
1435
|
tenantId;
|
|
1301
|
-
|
|
1436
|
+
locations = /* @__PURE__ */ new Map();
|
|
1302
1437
|
items = /* @__PURE__ */ new Map();
|
|
1303
|
-
|
|
1438
|
+
equipment = /* @__PURE__ */ new Map();
|
|
1439
|
+
/** 등급 정의(표준 `<X>Class`) — 상속·유효기간 판정의 재료. 선언 안 하면 비어 있고, 소속 그대로 판정한다. */
|
|
1440
|
+
classDefs = {};
|
|
1304
1441
|
/** 사람 — 선언하지 않으면 빈 맵(인원 제약 없는 트윈, 기존 거동). */
|
|
1305
1442
|
persons = /* @__PURE__ */ new Map();
|
|
1306
1443
|
/** 물리 자산 — 선언하지 않으면 빈 맵(자산 제약 없는 트윈, 기존 거동). */
|
|
@@ -1347,43 +1484,44 @@ var FlowEngine = class {
|
|
|
1347
1484
|
// ── TwinKernel (mechanics, 도메인 무관) ───────────────────────────────────
|
|
1348
1485
|
loadBoard(def) {
|
|
1349
1486
|
this.boardDef = def;
|
|
1350
|
-
for (const n of def
|
|
1351
|
-
|
|
1352
|
-
for (const
|
|
1353
|
-
for (const
|
|
1354
|
-
|
|
1487
|
+
for (const n of readBoardLocations(def)) this.locations.set(n.id, { id: n.id, type: n.type, capacity: n.capacity, parallelism: n.parallelism, occupancy: 0, status: "idle", parentId: n.parentId });
|
|
1488
|
+
this.classDefs = { personnel: def.personnelClasses, equipment: def.equipmentClasses, asset: def.assetClasses };
|
|
1489
|
+
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.homeLocation ? { location: p.homeLocation } : {}, ...p.properties ? { properties: p.properties } : {}, ...p.testSpecificationIds ? { testSpecificationIds: p.testSpecificationIds } : {} });
|
|
1490
|
+
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 } : {} });
|
|
1491
|
+
for (const m of readBoardEquipment(def)) {
|
|
1492
|
+
const eq = { id: m.id, kind: m.kind, location: m.homeLocation, homeLocation: m.homeLocation, ...m.properties ? { properties: m.properties } : {}, ...m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}, status: "idle", taskId: null, runMs: 0, setupMs: 0, downMs: 0, goodCount: 0, scrapCount: 0, window: m.window };
|
|
1355
1493
|
if (m.mtbfMs !== void 0) {
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1494
|
+
eq.mtbfMs = m.mtbfMs;
|
|
1495
|
+
eq.mttrMs = m.mttrMs;
|
|
1496
|
+
eq.nextFailureMs = this.sampleExp(m.mtbfMs);
|
|
1359
1497
|
}
|
|
1360
|
-
this.
|
|
1498
|
+
this.equipment.set(m.id, eq);
|
|
1361
1499
|
}
|
|
1362
1500
|
}
|
|
1363
1501
|
/**
|
|
1364
|
-
* what-if 구성 변주 — fork(또는 실행 중) 엔진에
|
|
1502
|
+
* what-if 구성 변주 — fork(또는 실행 중) 엔진에 설비 추가. loadBoard 설비 삽입과 동일 규약.
|
|
1365
1503
|
* 기본은 mtbf 미지정(고장 없는 신뢰 자원) → sampleExp(rng) 무소비라 baseline fork 와 깨끗이 비교 가능.
|
|
1366
1504
|
*/
|
|
1367
|
-
|
|
1368
|
-
if (this.
|
|
1369
|
-
const
|
|
1505
|
+
addEquipment(m) {
|
|
1506
|
+
if (this.equipment.has(m.id)) return;
|
|
1507
|
+
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
1508
|
if (m.mtbfMs !== void 0) {
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1509
|
+
eq.mtbfMs = m.mtbfMs;
|
|
1510
|
+
eq.mttrMs = m.mttrMs;
|
|
1511
|
+
eq.nextFailureMs = this.sampleExp(m.mtbfMs);
|
|
1374
1512
|
}
|
|
1375
|
-
this.
|
|
1513
|
+
this.equipment.set(m.id, eq);
|
|
1376
1514
|
}
|
|
1377
1515
|
/**
|
|
1378
|
-
* 관측 상태 주입(라이브 예측용, kernel-unification P1) — 외부 관측 스냅샷(
|
|
1516
|
+
* 관측 상태 주입(라이브 예측용, kernel-unification P1) — 외부 관측 스냅샷(재고·설비·자리)과
|
|
1379
1517
|
* 저널 오더(원값+라인)로 이 커널의 맵을 채운다. tick 으로 만든 게 아니라 "현재 관측된 현실"을 심어
|
|
1380
1518
|
* 이후 fork/tick 으로 예측한다. 라이브 런타임은 여전히 projector 미러 — 이 커널은 예측용 임시본.
|
|
1381
1519
|
* 오더는 남은 데맨드(라인별 requested-fulfilled)를 'created' 로 복원(현재 재고에서 재계획).
|
|
1382
1520
|
* 진행 중 개별 task 의 내부 상태는 관측만으론 복원 불가 → 재계획에 맡김(정직한 한계).
|
|
1383
1521
|
*/
|
|
1384
1522
|
hydrateObserved(snap, orders = []) {
|
|
1385
|
-
for (const n of snap.
|
|
1386
|
-
this.
|
|
1523
|
+
for (const n of snap.locations) {
|
|
1524
|
+
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
1525
|
}
|
|
1388
1526
|
this.items.clear();
|
|
1389
1527
|
for (const it of snap.items) {
|
|
@@ -1401,12 +1539,13 @@ var FlowEngine = class {
|
|
|
1401
1539
|
ilmd: it.ilmd
|
|
1402
1540
|
});
|
|
1403
1541
|
}
|
|
1404
|
-
for (const m of snap.
|
|
1542
|
+
for (const m of snap.equipment) {
|
|
1405
1543
|
const oee = m.oee;
|
|
1406
|
-
this.
|
|
1544
|
+
this.equipment.set(m.id, {
|
|
1407
1545
|
id: m.id,
|
|
1408
1546
|
kind: m.kind,
|
|
1409
1547
|
location: m.location ?? "",
|
|
1548
|
+
...m.homeLocation ? { homeLocation: m.homeLocation } : {},
|
|
1410
1549
|
status: m.status ?? "idle",
|
|
1411
1550
|
taskId: null,
|
|
1412
1551
|
runMs: oee?.runMs ?? 0,
|
|
@@ -1417,10 +1556,10 @@ var FlowEngine = class {
|
|
|
1417
1556
|
});
|
|
1418
1557
|
}
|
|
1419
1558
|
for (const p of snap.persons ?? []) {
|
|
1420
|
-
this.persons.set(p.id, { id: p.id,
|
|
1559
|
+
this.persons.set(p.id, { id: p.id, personnelClassIds: p.personnelClassIds, status: "idle", taskId: null });
|
|
1421
1560
|
}
|
|
1422
1561
|
for (const a of snap.assets ?? []) {
|
|
1423
|
-
this.assets.set(a.id, { id: a.id,
|
|
1562
|
+
this.assets.set(a.id, { id: a.id, assetClassIds: a.assetClassIds, location: a.location, status: "idle", taskId: null, carrying: a.carrying });
|
|
1424
1563
|
}
|
|
1425
1564
|
for (const t of snap.tasks ?? []) {
|
|
1426
1565
|
if (t.status === "completed") continue;
|
|
@@ -1440,7 +1579,7 @@ var FlowEngine = class {
|
|
|
1440
1579
|
intent: t.intent
|
|
1441
1580
|
});
|
|
1442
1581
|
if (known && t.status === "in-progress" && t.resourceRef) {
|
|
1443
|
-
const mv = this.
|
|
1582
|
+
const mv = this.equipment.get(t.resourceRef);
|
|
1444
1583
|
if (mv) {
|
|
1445
1584
|
mv.status = "busy";
|
|
1446
1585
|
mv.taskId = t.id;
|
|
@@ -1466,9 +1605,9 @@ var FlowEngine = class {
|
|
|
1466
1605
|
this.orders.set(o.orderId, { id: o.orderId, kind: o.kind, status: "created", requested: remaining, fulfilled: 0, bizTransaction: "", allocated: [], picked: [], shipmentEpc: null, lines });
|
|
1467
1606
|
}
|
|
1468
1607
|
}
|
|
1469
|
-
/** what-if 구성 변주 —
|
|
1470
|
-
|
|
1471
|
-
const n = this.
|
|
1608
|
+
/** what-if 구성 변주 — 자리 용량 변경(fork 대상). 존재하면 true. */
|
|
1609
|
+
setLocationCapacity(locationId, capacity) {
|
|
1610
|
+
const n = this.locations.get(locationId);
|
|
1472
1611
|
if (!n) return false;
|
|
1473
1612
|
n.capacity = Math.max(0, capacity);
|
|
1474
1613
|
return true;
|
|
@@ -1513,39 +1652,39 @@ var FlowEngine = class {
|
|
|
1513
1652
|
if (id) this._acked.add(id);
|
|
1514
1653
|
return ok();
|
|
1515
1654
|
}
|
|
1516
|
-
// Operable 코어 — 자원(
|
|
1655
|
+
// Operable 코어 — 자원(설비·설비) 제어. capability-keyed(resourceId), 모든 operable 자원 공통.
|
|
1517
1656
|
case CMD.resourceHold:
|
|
1518
1657
|
case CMD.resourceResume: {
|
|
1519
|
-
const m = this.
|
|
1658
|
+
const m = this.equipment.get(cmd.args?.resourceId ?? "");
|
|
1520
1659
|
if (!m) return fail("resource-not-found");
|
|
1521
1660
|
m.held = cmd.type === CMD.resourceHold;
|
|
1522
|
-
this.
|
|
1661
|
+
this.emitEquipment(m);
|
|
1523
1662
|
return ok();
|
|
1524
1663
|
}
|
|
1525
1664
|
case CMD.resourceDown: {
|
|
1526
1665
|
const a = cmd.args;
|
|
1527
|
-
const m = this.
|
|
1666
|
+
const m = this.equipment.get(a?.resourceId ?? "");
|
|
1528
1667
|
if (!m) return fail("resource-not-found");
|
|
1529
1668
|
if (m.status !== "down") {
|
|
1530
1669
|
m.status = "down";
|
|
1531
1670
|
m.repairUntilMs = this.clockMs + (Number(a?.durationMs) || m.mttrMs || 6e4);
|
|
1532
|
-
this.
|
|
1671
|
+
this.emitEquipment(m);
|
|
1533
1672
|
}
|
|
1534
1673
|
return ok();
|
|
1535
1674
|
}
|
|
1536
1675
|
case CMD.resourceRepair: {
|
|
1537
|
-
const m = this.
|
|
1676
|
+
const m = this.equipment.get(cmd.args?.resourceId ?? "");
|
|
1538
1677
|
if (!m) return fail("resource-not-found");
|
|
1539
1678
|
if (m.status === "down") {
|
|
1540
1679
|
m.status = m.taskId ? "busy" : "idle";
|
|
1541
1680
|
m.repairUntilMs = void 0;
|
|
1542
1681
|
if (m.mtbfMs !== void 0) m.nextFailureMs = this.clockMs + this.sampleExp(m.mtbfMs);
|
|
1543
|
-
this.
|
|
1682
|
+
this.emitEquipment(m);
|
|
1544
1683
|
}
|
|
1545
1684
|
return ok();
|
|
1546
1685
|
}
|
|
1547
1686
|
case CMD.resourceResetMetrics: {
|
|
1548
|
-
const m = this.
|
|
1687
|
+
const m = this.equipment.get(cmd.args?.resourceId ?? "");
|
|
1549
1688
|
if (!m) return fail("resource-not-found");
|
|
1550
1689
|
m.runMs = 0;
|
|
1551
1690
|
m.setupMs = 0;
|
|
@@ -1554,21 +1693,21 @@ var FlowEngine = class {
|
|
|
1554
1693
|
m.scrapCount = 0;
|
|
1555
1694
|
m.holdMs = 0;
|
|
1556
1695
|
m.metricsSinceMs = this.clockMs;
|
|
1557
|
-
this.
|
|
1696
|
+
this.emitEquipment(m);
|
|
1558
1697
|
return ok();
|
|
1559
1698
|
}
|
|
1560
1699
|
case CMD.resourceAdd: {
|
|
1561
1700
|
const a = cmd.args;
|
|
1562
1701
|
if (!a?.kind) return fail("kind-required");
|
|
1563
|
-
if (!a?.
|
|
1702
|
+
if (!a?.homeLocation || !this.locations.has(a.homeLocation)) return fail("home-location-not-found", { homeLocation: a?.homeLocation ?? "" });
|
|
1564
1703
|
const count = Math.max(1, Math.min(50, Number(a.count) || 1));
|
|
1565
|
-
let seq = this.
|
|
1704
|
+
let seq = this.equipment.size;
|
|
1566
1705
|
for (let i = 0; i < count; i++) {
|
|
1567
1706
|
let id = `${a.kind}-${++seq}`;
|
|
1568
|
-
while (this.
|
|
1569
|
-
this.
|
|
1570
|
-
const m = this.
|
|
1571
|
-
if (m) this.
|
|
1707
|
+
while (this.equipment.has(id)) id = `${a.kind}-${++seq}`;
|
|
1708
|
+
this.addEquipment({ id, kind: a.kind, homeLocation: a.homeLocation });
|
|
1709
|
+
const m = this.equipment.get(id);
|
|
1710
|
+
if (m) this.emitEquipment(m);
|
|
1572
1711
|
}
|
|
1573
1712
|
return ok();
|
|
1574
1713
|
}
|
|
@@ -1617,25 +1756,30 @@ var FlowEngine = class {
|
|
|
1617
1756
|
simClockMs: this.clockMs,
|
|
1618
1757
|
/* 출처 표시 — 보드(마스터)에서 온 자리다. 미러는 관측으로 알게 된 자리를 'observed' 로 구별하는데,
|
|
1619
1758
|
* 시뮬이 아무 표시도 안 하면 소비처가 두 스냅샷을 같은 규칙으로 읽지 못한다. */
|
|
1620
|
-
|
|
1759
|
+
locations: [...this.locations.values()].map((n) => {
|
|
1621
1760
|
const { status, ...rest } = n;
|
|
1622
|
-
const derived =
|
|
1761
|
+
const derived = locationStatusOf(n);
|
|
1623
1762
|
return { ...rest, ...derived ? { status: derived } : {}, origin: "master" };
|
|
1624
1763
|
}),
|
|
1625
1764
|
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 } : {} };
|
|
1765
|
+
equipment: [...this.equipment.values()].map((m) => {
|
|
1766
|
+
const s = { id: m.id, kind: m.kind, location: m.location, ...m.homeLocation ? { homeLocation: m.homeLocation } : {}, status: m.status, taskId: m.taskId ?? void 0, oee: this.oeeOf(m), held: m.held, ...m.properties ? { properties: m.properties } : {}, ...m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}, origin: "master", ...this.offShift(m) ? { offShift: true } : {} };
|
|
1628
1767
|
const t = m.taskId ? this.tasks.get(m.taskId) : void 0;
|
|
1629
1768
|
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
1769
|
return s;
|
|
1631
1770
|
}),
|
|
1632
1771
|
assets: [...this.assets.values()].map((a) => {
|
|
1633
|
-
const st = { id: a.id,
|
|
1772
|
+
const st = { id: a.id, assetClassIds: a.assetClassIds, location: a.location, status: a.status, taskId: a.taskId ?? void 0 };
|
|
1634
1773
|
if (a.carrying) st.carrying = a.carrying;
|
|
1774
|
+
if (a.properties) st.properties = a.properties;
|
|
1775
|
+
if (a.testSpecificationIds) st.testSpecificationIds = a.testSpecificationIds;
|
|
1635
1776
|
return st;
|
|
1636
1777
|
}),
|
|
1637
1778
|
persons: [...this.persons.values()].map((p) => {
|
|
1638
|
-
const st = { id: p.id,
|
|
1779
|
+
const st = { id: p.id, personnelClassIds: p.personnelClassIds, status: p.status, taskId: p.taskId ?? void 0 };
|
|
1780
|
+
if (p.location) st.location = p.location;
|
|
1781
|
+
if (p.properties) st.properties = p.properties;
|
|
1782
|
+
if (p.testSpecificationIds) st.testSpecificationIds = p.testSpecificationIds;
|
|
1639
1783
|
if (this.personOffShift(p)) st.offShift = true;
|
|
1640
1784
|
return st;
|
|
1641
1785
|
}),
|
|
@@ -1672,13 +1816,13 @@ var FlowEngine = class {
|
|
|
1672
1816
|
};
|
|
1673
1817
|
}
|
|
1674
1818
|
/*
|
|
1675
|
-
* 주목 신호 판단 — 상태(
|
|
1819
|
+
* 주목 신호 판단 — 상태(자리·설비·오더)에서 도메인 조건을 평가해 Attention 방출.
|
|
1676
1820
|
* severity=ISA-18.2 우선순위 계열, kind=도메인 라벨. UI 는 판단 안 함(임계값 여기 소유).
|
|
1677
1821
|
* 도메인별 추가 판단은 서브클래스가 override 로 확장(super.computeAttentions() 합성).
|
|
1678
1822
|
*/
|
|
1679
1823
|
computeAttentions() {
|
|
1680
1824
|
const out = deriveAttentions(
|
|
1681
|
-
{
|
|
1825
|
+
{ equipment: [...this.equipment.values()], locations: [...this.locations.values()], orders: [...this.orders.values()] },
|
|
1682
1826
|
this._acked
|
|
1683
1827
|
);
|
|
1684
1828
|
const present = new Set(out.map((a) => a.id));
|
|
@@ -1733,7 +1877,7 @@ var FlowEngine = class {
|
|
|
1733
1877
|
*/
|
|
1734
1878
|
apply(envelope) {
|
|
1735
1879
|
if (!this.observer) {
|
|
1736
|
-
this.observer = new ObservedReducer(this.boardDef ?? {
|
|
1880
|
+
this.observer = new ObservedReducer(this.boardDef ?? { locations: [], equipment: [] });
|
|
1737
1881
|
this.observeMode = true;
|
|
1738
1882
|
}
|
|
1739
1883
|
this.observer.apply(envelope);
|
|
@@ -1881,28 +2025,28 @@ var FlowEngine = class {
|
|
|
1881
2025
|
}
|
|
1882
2026
|
return mix[mix.length - 1].gtin;
|
|
1883
2027
|
}
|
|
1884
|
-
|
|
1885
|
-
for (const n of this.
|
|
2028
|
+
locationByType(type) {
|
|
2029
|
+
for (const n of this.locations.values()) if (n.type === type) return n;
|
|
1886
2030
|
return void 0;
|
|
1887
2031
|
}
|
|
1888
2032
|
/**
|
|
1889
2033
|
* process 변화 대수(EPCIS TransformationEvent · ISA-95 Material Consumed/Produced · 씬 Processable.transform).
|
|
1890
2034
|
* inputs 소비 → outputs 생산. 입출력 arity 가 곧 대수:
|
|
1891
2035
|
* merge N→1(조립) · split 1→N(분해) · transform 1→1(타입변경) · loss N→0(소실) · gain 0→N(부산물·생성).
|
|
1892
|
-
* 아이템 상태(소비/생산)와
|
|
2036
|
+
* 아이템 상태(소비/생산)와 자리 점유를 갱신하고 계보(TransformationEvent)를 방출한다.
|
|
1893
2037
|
* 도메인은 이 원시만 호출 — onTaskComplete 의 소비/생산/EPCIS 손코딩을 대체.
|
|
1894
2038
|
*/
|
|
1895
2039
|
transform(inputs, outputs, opts) {
|
|
1896
2040
|
for (const epc of inputs) {
|
|
1897
2041
|
const it = this.items.get(epc);
|
|
1898
2042
|
if (!it) continue;
|
|
1899
|
-
const n = this.
|
|
2043
|
+
const n = this.locations.get(it.location);
|
|
1900
2044
|
if (n) n.occupancy--;
|
|
1901
2045
|
this.items.delete(epc);
|
|
1902
2046
|
}
|
|
1903
2047
|
for (const o of outputs) {
|
|
1904
2048
|
this.items.set(o.epc, { epc: o.epc, gtin: o.gtin, qty: o.qty, location: o.location, disposition: o.disposition });
|
|
1905
|
-
const n = this.
|
|
2049
|
+
const n = this.locations.get(o.location);
|
|
1906
2050
|
if (n) n.occupancy++;
|
|
1907
2051
|
}
|
|
1908
2052
|
this.emit(transformationEvent({
|
|
@@ -1976,7 +2120,7 @@ var FlowEngine = class {
|
|
|
1976
2120
|
for (const c of children) {
|
|
1977
2121
|
const it = this.items.get(c);
|
|
1978
2122
|
if (it) {
|
|
1979
|
-
const n = this.
|
|
2123
|
+
const n = this.locations.get(it.location);
|
|
1980
2124
|
if (n) n.occupancy--;
|
|
1981
2125
|
this.items.delete(c);
|
|
1982
2126
|
}
|
|
@@ -1990,7 +2134,7 @@ var FlowEngine = class {
|
|
|
1990
2134
|
}
|
|
1991
2135
|
/**
|
|
1992
2136
|
* containment 분해(EPCIS AggregationEvent DELETE) — 부모(용기)에서 자식들을 풀어냄.
|
|
1993
|
-
* materialize 지정 시 자식이 독립 아이템으로 등장(materialize:
|
|
2137
|
+
* materialize 지정 시 자식이 독립 아이템으로 등장(materialize: 자리 배치 + occupancy + ObjectEvent ADD).
|
|
1994
2138
|
* 미지정 시 이벤트만.
|
|
1995
2139
|
*/
|
|
1996
2140
|
disaggregate(parent, children, opts) {
|
|
@@ -2003,7 +2147,7 @@ var FlowEngine = class {
|
|
|
2003
2147
|
const m = opts.materialize;
|
|
2004
2148
|
for (const c of children) {
|
|
2005
2149
|
this.items.set(c, { epc: c, location: m.location, disposition: m.disposition });
|
|
2006
|
-
const n = this.
|
|
2150
|
+
const n = this.locations.get(m.location);
|
|
2007
2151
|
if (n) n.occupancy++;
|
|
2008
2152
|
this.emit(objectEvent({ eventTime: this.now(), action: "ADD", bizStep: opts.bizStep, disposition: m.disposition, epcList: [c], readPoint: m.location, bizLocation: m.location }));
|
|
2009
2153
|
}
|
|
@@ -2011,22 +2155,22 @@ var FlowEngine = class {
|
|
|
2011
2155
|
}
|
|
2012
2156
|
/** 품질 보고(OEE Quality) — 도메인이 완료 시 자원별 양품/불량 1건 계상(예: 용접 수율). */
|
|
2013
2157
|
recordOutput(moverId, good) {
|
|
2014
|
-
const m = moverId ? this.
|
|
2158
|
+
const m = moverId ? this.equipment.get(moverId) : void 0;
|
|
2015
2159
|
if (!m) return;
|
|
2016
2160
|
if (good) m.goodCount++;
|
|
2017
2161
|
else m.scrapCount++;
|
|
2018
2162
|
this.emitOp(OP_EVENT.quality, { moverId: m.id, good, goodCount: m.goodCount, scrapCount: m.scrapCount });
|
|
2019
2163
|
}
|
|
2020
|
-
/**
|
|
2164
|
+
/** 설비 OEE(스냅샷 파생) — Availability×Performance×Quality. planned = 설비 존재 sim 시간(clockMs). */
|
|
2021
2165
|
oeeOf(m) {
|
|
2022
2166
|
return computeOee(m, this.clockMs);
|
|
2023
2167
|
}
|
|
2024
|
-
/** 정책에 넘길 특정 타입
|
|
2025
|
-
slotViews(
|
|
2168
|
+
/** 정책에 넘길 특정 타입 자리의 관측 뷰 — 예약(그 자리로 향하는 in-flight task) 포함. */
|
|
2169
|
+
slotViews(locationType) {
|
|
2026
2170
|
const reserved = /* @__PURE__ */ new Map();
|
|
2027
2171
|
for (const t of this.tasks.values()) if (t.status !== "completed") reserved.set(t.toNode, (reserved.get(t.toNode) ?? 0) + 1);
|
|
2028
2172
|
const views = [];
|
|
2029
|
-
for (const n of this.
|
|
2173
|
+
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
2174
|
return views;
|
|
2031
2175
|
}
|
|
2032
2176
|
emit(event) {
|
|
@@ -2068,15 +2212,15 @@ var FlowEngine = class {
|
|
|
2068
2212
|
}
|
|
2069
2213
|
/** 사람 상태 전이 — 설비와 별개 채널(어휘가 다르다: 고장이 아니라 교대·투입). */
|
|
2070
2214
|
emitAsset(a) {
|
|
2071
|
-
this.emitOp(OP_EVENT.asset, { assetId: a.id,
|
|
2215
|
+
this.emitOp(OP_EVENT.asset, { assetId: a.id, assetClassIds: a.assetClassIds, status: a.status, location: a.location, taskId: a.taskId ?? void 0, carrying: a.carrying });
|
|
2072
2216
|
}
|
|
2073
2217
|
emitPerson(p) {
|
|
2074
|
-
this.emitOp(OP_EVENT.person, { personId: p.id,
|
|
2218
|
+
this.emitOp(OP_EVENT.person, { personId: p.id, personnelClassIds: p.personnelClassIds, status: p.status, taskId: p.taskId ?? void 0, ...p.location ? { location: p.location } : {}, ...this.personOffShift(p) ? { offShift: true } : {} });
|
|
2075
2219
|
}
|
|
2076
2220
|
/** 설비 상태 전이 — `taskId` 를 함께 싣는다(사람·자산 델타와 같은 규칙). 없으면 미러가 "이 설비가
|
|
2077
2221
|
* 무슨 일을 하는 중인가" 를 알 수 없어 작업↔자원 연결이 한쪽에서만 성립한다. */
|
|
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 });
|
|
2222
|
+
emitEquipment(m, motion) {
|
|
2223
|
+
this.emitOp(OP_EVENT.equipment, { moverId: m.id, kind: m.kind, status: m.status, location: m.location, ...m.homeLocation ? { homeLocation: m.homeLocation } : {}, taskId: m.taskId ?? void 0, motion });
|
|
2080
2224
|
}
|
|
2081
2225
|
/** 오더 델타 — **라인까지 싣는다.** 라인이 빠지면 미러가 남은 데맨드를 라인별로 재계획할 수 없다. */
|
|
2082
2226
|
emitOrder(o) {
|
|
@@ -2156,7 +2300,8 @@ var FlowEngine = class {
|
|
|
2156
2300
|
const want = Math.max(0, Math.floor(req.quantity ?? 0));
|
|
2157
2301
|
if (!want) continue;
|
|
2158
2302
|
const avail = [...this.assets.values()].filter(
|
|
2159
|
-
|
|
2303
|
+
/* 인원과 같은 규칙 — 상속을 타고 닫아 판정하고 유효기간 밖은 제외한다. 요구는 등급 하나+수량. */
|
|
2304
|
+
(a) => a.status === "idle" && !picked.includes(a.id) && (req.assetClass === void 0 || classClosure(a.assetClassIds, this.classDefs.asset, this.now()).has(req.assetClass))
|
|
2160
2305
|
);
|
|
2161
2306
|
if (avail.length < want) return null;
|
|
2162
2307
|
for (let i = 0; i < want; i++) picked.push(avail[i].id);
|
|
@@ -2213,7 +2358,10 @@ var FlowEngine = class {
|
|
|
2213
2358
|
const want = Math.max(0, Math.floor(req.quantity ?? 0));
|
|
2214
2359
|
if (!want) continue;
|
|
2215
2360
|
const avail = [...this.persons.values()].filter(
|
|
2216
|
-
(p) => p.status === "idle" && !picked.includes(p.id) && !this.personOffShift(p) &&
|
|
2361
|
+
(p) => p.status === "idle" && !picked.includes(p.id) && !this.personOffShift(p) && /* 자격은 **상속을 타고 닫아** 판정한다 — 사람은 여러 등급에 속할 수 있고(표준), 등급은
|
|
2362
|
+
다른 등급을 상속한다. "생산직 2명" 요구를 "용접 자격자" 가 만족해야 한다.
|
|
2363
|
+
유효기간 밖의 등급은 닫힘에서 빠진다(만료된 자격으로 배정되지 않는다). */
|
|
2364
|
+
(req.personnelClass === void 0 || classClosure(p.personnelClassIds, this.classDefs.personnel, this.now()).has(req.personnelClass))
|
|
2217
2365
|
);
|
|
2218
2366
|
if (avail.length < want) return null;
|
|
2219
2367
|
for (let i = 0; i < want; i++) picked.push(avail[i].id);
|
|
@@ -2226,8 +2374,11 @@ var FlowEngine = class {
|
|
|
2226
2374
|
* 여기서는 고르기만 한다 — 확정은 호출부가 다른 자원까지 확보한 뒤에 한다.
|
|
2227
2375
|
*/
|
|
2228
2376
|
claimEquipment(t) {
|
|
2229
|
-
const free = (kind, picked2 = []) => [...this.
|
|
2230
|
-
(m) => m.status === "idle" && !m.held && !this.offShift(m) && !picked2.includes(m.id) && (kind
|
|
2377
|
+
const free = (kind, picked2 = []) => [...this.equipment.values()].filter(
|
|
2378
|
+
(m) => m.status === "idle" && !m.held && !this.offShift(m) && !picked2.includes(m.id) && /* 설비의 소속은 아직 단수(`kind`)다 — 저널에 기록이 쌓여 복수화를 별도 작업으로 두었다.
|
|
2379
|
+
다만 등급 정의가 있으면 **상속은 지금도 탄다**: 'welder-6axis' 가 'welder' 를 상속하면
|
|
2380
|
+
'welder' 요구를 만족한다. 단수/복수와 상속은 다른 축이므로 하나를 기다리지 않는다. */
|
|
2381
|
+
(kind === void 0 || classClosure(m.kind ? [m.kind] : [], this.classDefs.equipment, this.now()).has(kind))
|
|
2231
2382
|
);
|
|
2232
2383
|
const need = this.operationSpecs.get(t.kind)?.equipmentSpecification;
|
|
2233
2384
|
if (!need?.length) {
|
|
@@ -2277,12 +2428,12 @@ var FlowEngine = class {
|
|
|
2277
2428
|
* 이 자리가 동시 처리 한도에 찼는가 — `parallelism` 을 선언한 자리만 판정한다(미선언=제약 없음).
|
|
2278
2429
|
* 세는 대상은 **그 자리에서 진행 중인 작업**(`toNode` 기준, in-progress). 대기 중인 작업은 세지 않는다.
|
|
2279
2430
|
*/
|
|
2280
|
-
stationFull(
|
|
2281
|
-
if (!
|
|
2282
|
-
const limit = this.
|
|
2431
|
+
stationFull(locationId) {
|
|
2432
|
+
if (!locationId) return false;
|
|
2433
|
+
const limit = this.locations.get(locationId)?.parallelism;
|
|
2283
2434
|
if (!(typeof limit === "number" && limit > 0)) return false;
|
|
2284
2435
|
let running = 0;
|
|
2285
|
-
for (const t of this.tasks.values()) if (t.status === "in-progress" && t.toNode ===
|
|
2436
|
+
for (const t of this.tasks.values()) if (t.status === "in-progress" && t.toNode === locationId) running++;
|
|
2286
2437
|
return running >= limit;
|
|
2287
2438
|
}
|
|
2288
2439
|
/** 교대 밖인가 — 자원이 지금 일하지 않는 이유 중 고장·계획정지와 구별되는 세 번째. */
|
|
@@ -2312,19 +2463,19 @@ var FlowEngine = class {
|
|
|
2312
2463
|
return -Math.log(1 - this.rng()) * meanMs;
|
|
2313
2464
|
}
|
|
2314
2465
|
/**
|
|
2315
|
-
* 확률적 설비 고장(MTBF/MTTR) — mtbf 지정
|
|
2466
|
+
* 확률적 설비 고장(MTBF/MTTR) — mtbf 지정 설비만 참여(미지정=고장 없음, rng 무소비 → byte-identical).
|
|
2316
2467
|
* up: nextFailure 도래 시 down(수리까지 repairUntil). down: downMs 누적, repair 도래 시 up(다음 고장 예약).
|
|
2317
|
-
* down 중
|
|
2468
|
+
* down 중 설비는 배정 불가 + 진행중 task 동결(processTasks 가 skip) → OEE Availability 손실.
|
|
2318
2469
|
*/
|
|
2319
2470
|
processFailures(dt) {
|
|
2320
|
-
for (const m of this.
|
|
2471
|
+
for (const m of this.equipment.values()) {
|
|
2321
2472
|
if (m.status === "down") {
|
|
2322
2473
|
m.downMs += dt;
|
|
2323
2474
|
if (m.repairUntilMs != null && this.clockMs >= m.repairUntilMs) {
|
|
2324
2475
|
m.status = m.taskId ? "busy" : "idle";
|
|
2325
2476
|
m.repairUntilMs = void 0;
|
|
2326
2477
|
if (m.mtbfMs !== void 0) m.nextFailureMs = this.clockMs + this.sampleExp(m.mtbfMs);
|
|
2327
|
-
this.
|
|
2478
|
+
this.emitEquipment(m);
|
|
2328
2479
|
}
|
|
2329
2480
|
continue;
|
|
2330
2481
|
}
|
|
@@ -2336,7 +2487,7 @@ var FlowEngine = class {
|
|
|
2336
2487
|
m.status = "down";
|
|
2337
2488
|
m.repairUntilMs = this.clockMs + this.sampleExp(m.mttrMs ?? m.mtbfMs);
|
|
2338
2489
|
m.nextFailureMs = void 0;
|
|
2339
|
-
this.
|
|
2490
|
+
this.emitEquipment(m);
|
|
2340
2491
|
}
|
|
2341
2492
|
}
|
|
2342
2493
|
}
|
|
@@ -2422,34 +2573,34 @@ var FlowEngine = class {
|
|
|
2422
2573
|
if (this.stationFull(t.toNode)) continue;
|
|
2423
2574
|
const rigs = this.claimEquipment(t);
|
|
2424
2575
|
if (!rigs) continue;
|
|
2425
|
-
const
|
|
2426
|
-
if (t.setupMs && t.changeoverKey !== void 0 &&
|
|
2576
|
+
const eq = this.equipment.get(rigs[0]);
|
|
2577
|
+
if (t.setupMs && t.changeoverKey !== void 0 && eq.lastChangeoverKey !== void 0 && eq.lastChangeoverKey !== t.changeoverKey) {
|
|
2427
2578
|
t.appliedSetupMs = t.setupMs;
|
|
2428
2579
|
t.durationMs += t.setupMs;
|
|
2429
2580
|
}
|
|
2430
|
-
if (t.changeoverKey !== void 0)
|
|
2581
|
+
if (t.changeoverKey !== void 0) eq.lastChangeoverKey = t.changeoverKey;
|
|
2431
2582
|
for (const id of rigs) {
|
|
2432
|
-
const m = this.
|
|
2583
|
+
const m = this.equipment.get(id);
|
|
2433
2584
|
m.status = "busy";
|
|
2434
2585
|
m.taskId = t.id;
|
|
2435
2586
|
}
|
|
2436
2587
|
t.status = "in-progress";
|
|
2437
|
-
t.resource =
|
|
2588
|
+
t.resource = eq.id;
|
|
2438
2589
|
t.remainingMs = t.durationMs;
|
|
2439
2590
|
t.startedAtSimMs = this.clockMs;
|
|
2440
2591
|
if (rigs.length > 1) t.resources = rigs.slice();
|
|
2441
2592
|
this.assignCrew(t, crew);
|
|
2442
2593
|
this.assignAssets(t, gear);
|
|
2443
2594
|
this.emitTask(t);
|
|
2444
|
-
if (t.intent === "process") this.
|
|
2445
|
-
else this.
|
|
2595
|
+
if (t.intent === "process") this.emitEquipment(eq);
|
|
2596
|
+
else this.emitEquipment(eq, { fromNode: t.fromNode, toNode: t.toNode, startedAtSimMs: this.clockMs, durationMs: t.durationMs, progress: 0, elapsedMs: 0 });
|
|
2446
2597
|
}
|
|
2447
2598
|
}
|
|
2448
2599
|
/** in-progress 진행/완료 — 상태 효과는 전부 도메인(onTaskComplete). base 는 자원 해제·델타만. */
|
|
2449
2600
|
advanceTasks(dt) {
|
|
2450
2601
|
for (const t of this.tasks.values()) {
|
|
2451
2602
|
if (t.status !== "in-progress") continue;
|
|
2452
|
-
if (t.resource && this.
|
|
2603
|
+
if (t.resource && this.equipment.get(t.resource)?.status === "down") continue;
|
|
2453
2604
|
t.remainingMs -= dt;
|
|
2454
2605
|
if (t.remainingMs > 0) continue;
|
|
2455
2606
|
this.onTaskComplete(t);
|
|
@@ -2460,26 +2611,26 @@ var FlowEngine = class {
|
|
|
2460
2611
|
this.emitTask(t);
|
|
2461
2612
|
continue;
|
|
2462
2613
|
}
|
|
2463
|
-
const
|
|
2614
|
+
const eq = this.equipment.get(t.resource);
|
|
2464
2615
|
const setup = t.appliedSetupMs ?? 0;
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
if (t.intent !== "process")
|
|
2616
|
+
eq.setupMs += setup;
|
|
2617
|
+
eq.runMs += t.durationMs - setup;
|
|
2618
|
+
eq.status = "idle";
|
|
2619
|
+
eq.taskId = null;
|
|
2620
|
+
if (t.intent !== "process") eq.location = t.toNode;
|
|
2470
2621
|
for (const id of t.resources ?? []) {
|
|
2471
|
-
if (id ===
|
|
2472
|
-
const m = this.
|
|
2622
|
+
if (id === eq.id) continue;
|
|
2623
|
+
const m = this.equipment.get(id);
|
|
2473
2624
|
if (!m) continue;
|
|
2474
2625
|
m.setupMs += setup;
|
|
2475
2626
|
m.runMs += t.durationMs - setup;
|
|
2476
2627
|
m.status = "idle";
|
|
2477
2628
|
m.taskId = null;
|
|
2478
2629
|
if (t.intent !== "process") m.location = t.toNode;
|
|
2479
|
-
this.
|
|
2630
|
+
this.emitEquipment(m);
|
|
2480
2631
|
}
|
|
2481
2632
|
this.emitTask(t);
|
|
2482
|
-
this.
|
|
2633
|
+
this.emitEquipment(eq);
|
|
2483
2634
|
}
|
|
2484
2635
|
}
|
|
2485
2636
|
};
|
|
@@ -2496,7 +2647,7 @@ var WmsKernel = class extends FlowEngine {
|
|
|
2496
2647
|
}
|
|
2497
2648
|
/** 입고 도착 — §4 라이프사이클: ASN(PO) → 팔레트 조립 → 수령 → putaway task. */
|
|
2498
2649
|
onArrival(spec) {
|
|
2499
|
-
const dock = this.
|
|
2650
|
+
const dock = this.locationByType("dock");
|
|
2500
2651
|
if (!dock) return;
|
|
2501
2652
|
const epc = ssccUri(COMPANY_PREFIX, ++this.epcSeq);
|
|
2502
2653
|
const gtin = this.pickGtin(spec.content.skuMix);
|
|
@@ -2586,14 +2737,14 @@ var WmsKernel = class extends FlowEngine {
|
|
|
2586
2737
|
* 멀티SKU 는 여러 라인의 팔레트를 한 오더로 모아 단일 출하(finalizeOrder 통합 화물).
|
|
2587
2738
|
*/
|
|
2588
2739
|
allocate(o) {
|
|
2589
|
-
const staging = this.
|
|
2740
|
+
const staging = this.locationByType("staging");
|
|
2590
2741
|
if (!staging || !o.lines) return;
|
|
2591
2742
|
const chosenAll = [];
|
|
2592
2743
|
for (const line of o.lines) {
|
|
2593
2744
|
const already = o.allocated.filter((e) => this.items.get(e)?.gtin === line.gtin).length;
|
|
2594
2745
|
const need = line.requested - already;
|
|
2595
2746
|
if (need <= 0) continue;
|
|
2596
|
-
const available = [...this.items.values()].filter((i) => i.gtin === line.gtin && i.disposition === DISP.sellable && this.
|
|
2747
|
+
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
2748
|
const chosen = this.policy.selectStock({ gtin: line.gtin, qty: need, available });
|
|
2598
2749
|
for (const epc of chosen) {
|
|
2599
2750
|
o.allocated.push(epc);
|
|
@@ -2616,8 +2767,8 @@ var WmsKernel = class extends FlowEngine {
|
|
|
2616
2767
|
/** 태스크 완료 — 이동 반영 후 putaway=storing, pick=picking(+전량 시 pack→stage→ship). */
|
|
2617
2768
|
onTaskComplete(t) {
|
|
2618
2769
|
const item = this.items.get(t.itemEpc);
|
|
2619
|
-
const from = this.
|
|
2620
|
-
const to = this.
|
|
2770
|
+
const from = this.locations.get(t.fromNode);
|
|
2771
|
+
const to = this.locations.get(t.toNode);
|
|
2621
2772
|
from.occupancy--;
|
|
2622
2773
|
to.occupancy++;
|
|
2623
2774
|
item.location = to.id;
|
|
@@ -2635,7 +2786,7 @@ var WmsKernel = class extends FlowEngine {
|
|
|
2635
2786
|
}
|
|
2636
2787
|
/** 전량 피킹 → packing(조립)·staging·shipping 마감. 화물 사이트 이탈, 백오더 잔량 재할당. */
|
|
2637
2788
|
finalizeOrder(order, staging) {
|
|
2638
|
-
const shipDock = this.
|
|
2789
|
+
const shipDock = this.locationByType("dock-ship") ?? staging;
|
|
2639
2790
|
const eventTime = this.now();
|
|
2640
2791
|
const shipment = ssccUri(COMPANY_PREFIX, ++this.epcSeq);
|
|
2641
2792
|
order.shipmentEpc = shipment;
|
|
@@ -2693,8 +2844,8 @@ var YmsKernel = class extends FlowEngine {
|
|
|
2693
2844
|
}
|
|
2694
2845
|
/** 게이트-인 — 트레일러 도착 → 어포인트먼트(도크도어 배정 + 창). drop 은 야드 주차 태스크, live 는 게이트 대기. */
|
|
2695
2846
|
spawnAppointment(kind) {
|
|
2696
|
-
const gate = this.
|
|
2697
|
-
const doors = [...this.
|
|
2847
|
+
const gate = this.locationByType("gate");
|
|
2848
|
+
const doors = [...this.locations.values()].filter((n) => n.type === "dock-door").sort((a, b) => a.id.localeCompare(b.id));
|
|
2698
2849
|
if (!gate || doors.length === 0) return;
|
|
2699
2850
|
const inbound = kind === "appointment";
|
|
2700
2851
|
const epc = graiUri(CP, "10", ++this.epcSeq);
|
|
@@ -2731,15 +2882,15 @@ var YmsKernel = class extends FlowEngine {
|
|
|
2731
2882
|
const trailer = this.items.get(o.allocated[0]);
|
|
2732
2883
|
if (!trailer) return;
|
|
2733
2884
|
if (this.clockMs < (o.windowStartMs ?? 0)) return;
|
|
2734
|
-
const door = this.
|
|
2885
|
+
const door = this.locations.get(o.dockDoor);
|
|
2735
2886
|
const inflight = [...this.tasks.values()].some((t) => t.status !== "completed" && t.toNode === door.id);
|
|
2736
2887
|
if (door.occupancy > 0 || inflight) return;
|
|
2737
2888
|
const mode = this.apptMode.get(o.id) ?? "drop";
|
|
2738
|
-
if (mode === "drop" && this.
|
|
2889
|
+
if (mode === "drop" && this.locations.get(trailer.location)?.type !== "yard-slot") return;
|
|
2739
2890
|
const outbound = o.kind === "appointment-out";
|
|
2740
2891
|
const bizStep = outbound ? YARD_BIZSTEP.loading : YARD_BIZSTEP.unloading;
|
|
2741
2892
|
if (outbound) {
|
|
2742
|
-
const staging = this.
|
|
2893
|
+
const staging = this.locationByType("staging");
|
|
2743
2894
|
const reserved = new Set([...this.trailerCargo.values()].flat());
|
|
2744
2895
|
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
2896
|
if (!staging || avail.length < CARGO_PER_TRAILER) return;
|
|
@@ -2757,13 +2908,13 @@ var YmsKernel = class extends FlowEngine {
|
|
|
2757
2908
|
/** 태스크 완료 — 목적지 타입으로 분기: yard-slot=주차, dock-door=상/하차+depart, gate=출차. */
|
|
2758
2909
|
onTaskComplete(t) {
|
|
2759
2910
|
const trailer = this.items.get(t.itemEpc);
|
|
2760
|
-
const from = this.
|
|
2761
|
-
const to = this.
|
|
2911
|
+
const from = this.locations.get(t.fromNode);
|
|
2912
|
+
const to = this.locations.get(t.toNode);
|
|
2762
2913
|
from.occupancy--;
|
|
2763
2914
|
to.occupancy++;
|
|
2764
2915
|
trailer.location = to.id;
|
|
2765
2916
|
if (t.kind === "dwell") {
|
|
2766
|
-
const gate = this.
|
|
2917
|
+
const gate = this.locationByType("gate");
|
|
2767
2918
|
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
2919
|
this.tasks.set(dep.id, dep);
|
|
2769
2920
|
this.emitTask(dep);
|
|
@@ -2777,7 +2928,7 @@ var YmsKernel = class extends FlowEngine {
|
|
|
2777
2928
|
if (to.type === "dock-door") {
|
|
2778
2929
|
const order2 = t.orderId ? this.orders.get(t.orderId) : void 0;
|
|
2779
2930
|
const outbound = order2?.kind === "appointment-out";
|
|
2780
|
-
const staging = this.
|
|
2931
|
+
const staging = this.locationByType("staging");
|
|
2781
2932
|
const cargo = this.trailerCargo.get(trailer.epc) ?? [];
|
|
2782
2933
|
this.observeDisposition([trailer.epc], DISP.in_progress, outbound ? YARD_BIZSTEP.loading : YARD_BIZSTEP.unloading, to.id);
|
|
2783
2934
|
if (outbound) {
|
|
@@ -2829,10 +2980,10 @@ var MES_PART_GTINS = { partA: PART_A.gtin, partB: PART_B.gtin };
|
|
|
2829
2980
|
var MES_PRODUCT_GTINS = { p1: PRODUCTS[0].gtin, p2: PRODUCTS[1].gtin };
|
|
2830
2981
|
var MES_PRODUCTS = PRODUCTS.map((p) => ({ gtin: p.gtin, label: p.key }));
|
|
2831
2982
|
var ROUTE = [
|
|
2832
|
-
{ kind: "cut",
|
|
2833
|
-
{ kind: "weld",
|
|
2834
|
-
{ kind: "paint",
|
|
2835
|
-
{ kind: "assembly",
|
|
2983
|
+
{ kind: "cut", locationType: "cut-station", resource: "cutter" },
|
|
2984
|
+
{ kind: "weld", locationType: "weld-station", resource: "welder" },
|
|
2985
|
+
{ kind: "paint", locationType: "paint-booth", resource: "painter" },
|
|
2986
|
+
{ kind: "assembly", locationType: "assembly-line", resource: "assembler" }
|
|
2836
2987
|
];
|
|
2837
2988
|
var MesKernel = class extends FlowEngine {
|
|
2838
2989
|
wipSeq = 0;
|
|
@@ -2857,12 +3008,12 @@ var MesKernel = class extends FlowEngine {
|
|
|
2857
3008
|
if (cmd.type === MES_CMD.changeover) {
|
|
2858
3009
|
const a = cmd.args;
|
|
2859
3010
|
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.
|
|
3011
|
+
const m = this.equipment.get(a.resourceId);
|
|
2861
3012
|
if (!m) return { commandId: cmd.commandId, accepted: false, errorCode: "resource-not-found", errorParams: { resourceId: a.resourceId }, error: `resource-not-found: ${a.resourceId}` };
|
|
2862
3013
|
if (m.lastChangeoverKey !== a.gtin) {
|
|
2863
3014
|
m.setupMs += DEFAULT_SETUP_MS;
|
|
2864
3015
|
m.lastChangeoverKey = a.gtin;
|
|
2865
|
-
this.
|
|
3016
|
+
this.emitEquipment(m);
|
|
2866
3017
|
}
|
|
2867
3018
|
return { commandId: cmd.commandId, accepted: true };
|
|
2868
3019
|
}
|
|
@@ -2871,7 +3022,7 @@ var MesKernel = class extends FlowEngine {
|
|
|
2871
3022
|
/** 부품 수령(다품종) — skuMix 의 gtin 으로 부품 종류 결정. */
|
|
2872
3023
|
onArrival(spec) {
|
|
2873
3024
|
if (this.mesSpec) return this.onArrivalDef(spec);
|
|
2874
|
-
const rawStore = this.
|
|
3025
|
+
const rawStore = this.locationByType("raw-store");
|
|
2875
3026
|
if (!rawStore) return;
|
|
2876
3027
|
const gtin = this.pickGtin(spec.content.skuMix);
|
|
2877
3028
|
const part = [PART_A, PART_B].find((p) => p.gtin === gtin);
|
|
@@ -2895,12 +3046,12 @@ var MesKernel = class extends FlowEngine {
|
|
|
2895
3046
|
allocate(o) {
|
|
2896
3047
|
if (this.mesSpec) return this.allocateDef(o);
|
|
2897
3048
|
const s0 = ROUTE[0];
|
|
2898
|
-
const first = this.
|
|
3049
|
+
const first = this.locationByType(s0.locationType);
|
|
2899
3050
|
const product = this.productOf(o.gtin);
|
|
2900
3051
|
if (!first || !product) return;
|
|
2901
3052
|
const picks = [];
|
|
2902
3053
|
for (const line of product.bom) {
|
|
2903
|
-
const available = [...this.items.values()].filter((i) => i.gtin === line.part.gtin && i.disposition === DISP.sellable && this.
|
|
3054
|
+
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
3055
|
const chosen = this.policy.selectStock({ gtin: line.part.gtin, qty: line.qty, available });
|
|
2905
3056
|
if (chosen.length < line.qty) return;
|
|
2906
3057
|
picks.push(...chosen);
|
|
@@ -2914,8 +3065,8 @@ var MesKernel = class extends FlowEngine {
|
|
|
2914
3065
|
}
|
|
2915
3066
|
/** 라우트 스테이션 태스크 발행(공통) — 제자리 가공(process), 이종 자원, 제품 전환 셋업. */
|
|
2916
3067
|
emitStation(o, stage, itemEpc, changeoverKey) {
|
|
2917
|
-
const
|
|
2918
|
-
const task = { id: `task-${++this.taskSeq}`, kind: stage.kind, status: "created", itemEpc, fromNode:
|
|
3068
|
+
const loc = this.locationByType(stage.locationType);
|
|
3069
|
+
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
3070
|
this.tasks.set(task.id, task);
|
|
2920
3071
|
this.emitTask(task);
|
|
2921
3072
|
}
|
|
@@ -2925,12 +3076,12 @@ var MesKernel = class extends FlowEngine {
|
|
|
2925
3076
|
const order = this.orders.get(t.orderId);
|
|
2926
3077
|
const product = this.productOf(order.gtin);
|
|
2927
3078
|
const i = ROUTE.findIndex((s) => s.kind === t.kind);
|
|
2928
|
-
const
|
|
3079
|
+
const loc = this.locations.get(t.toNode);
|
|
2929
3080
|
const isLast = i === ROUTE.length - 1;
|
|
2930
3081
|
if (!isLast) {
|
|
2931
3082
|
const inputs = order.allocated.slice();
|
|
2932
3083
|
const wip2 = sgtinUri(CP2, WIP_ITEMREF, ++this.wipSeq);
|
|
2933
|
-
this.transform(inputs, [{ epc: wip2, gtin: WIP_GTIN, qty: 1, location:
|
|
3084
|
+
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
3085
|
order.allocated = [wip2];
|
|
2935
3086
|
const next = ROUTE[i + 1];
|
|
2936
3087
|
this.emitStation(order, next, wip2, product.gtin);
|
|
@@ -2938,13 +3089,13 @@ var MesKernel = class extends FlowEngine {
|
|
|
2938
3089
|
this.emitOrder(order);
|
|
2939
3090
|
return;
|
|
2940
3091
|
}
|
|
2941
|
-
const fgStore = this.
|
|
3092
|
+
const fgStore = this.locationByType("fg-store");
|
|
2942
3093
|
const wip = order.allocated[0];
|
|
2943
3094
|
const good = this.rng() < (this.paramNumber(t.kind, OP_PARAM.yield) ?? DEFAULT_YIELD);
|
|
2944
3095
|
this.recordOutput(t.resource, good);
|
|
2945
3096
|
const disp = good ? DISP.sellable : DISP.non_sellable;
|
|
2946
3097
|
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:
|
|
3098
|
+
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
3099
|
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
3100
|
order.allocated = [];
|
|
2950
3101
|
order.fulfilled = 1;
|
|
@@ -2971,7 +3122,7 @@ var MesKernel = class extends FlowEngine {
|
|
|
2971
3122
|
}
|
|
2972
3123
|
/** 정의 모드 수령 — skuMix gtin 이 레시피 입력 자재면 raw-store 에 생성. */
|
|
2973
3124
|
onArrivalDef(spec) {
|
|
2974
|
-
const rawStore = this.
|
|
3125
|
+
const rawStore = this.locationByType("raw-store");
|
|
2975
3126
|
if (!rawStore) return;
|
|
2976
3127
|
const gtin = this.pickGtin(spec.content.skuMix);
|
|
2977
3128
|
const inputKey = this.recipeDef().inputs.map((i) => i.material).find((k) => this.classOf(k) === gtin);
|
|
@@ -2993,12 +3144,12 @@ var MesKernel = class extends FlowEngine {
|
|
|
2993
3144
|
/** 정의 모드 할당 — 레시피 입력 BOM 전량 확보 후 첫 라우트 스텝 태스크. */
|
|
2994
3145
|
allocateDef(o) {
|
|
2995
3146
|
const ops = this.routeOps();
|
|
2996
|
-
if (!ops.length || !ops[0].
|
|
3147
|
+
if (!ops.length || !ops[0].locationType || !this.locationByType(ops[0].locationType)) return;
|
|
2997
3148
|
const rc = this.recipeDef();
|
|
2998
3149
|
const picks = [];
|
|
2999
3150
|
for (const line of rc.inputs) {
|
|
3000
3151
|
const g = this.classOf(line.material);
|
|
3001
|
-
const available = [...this.items.values()].filter((i) => i.gtin === g && i.disposition === DISP.sellable && this.
|
|
3152
|
+
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
3153
|
const chosen = this.policy.selectStock({ gtin: g, qty: line.qty, available });
|
|
3003
3154
|
if (chosen.length < line.qty) return;
|
|
3004
3155
|
picks.push(...chosen);
|
|
@@ -3011,8 +3162,8 @@ var MesKernel = class extends FlowEngine {
|
|
|
3011
3162
|
this.emitOrder(o);
|
|
3012
3163
|
}
|
|
3013
3164
|
emitStationDef(o, op, itemEpc) {
|
|
3014
|
-
const
|
|
3015
|
-
const task = { id: `task-${++this.taskSeq}`, kind: op.key, status: "created", itemEpc, fromNode:
|
|
3165
|
+
const loc = this.locationByType(op.locationType);
|
|
3166
|
+
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
3167
|
this.tasks.set(task.id, task);
|
|
3017
3168
|
this.emitTask(task);
|
|
3018
3169
|
}
|
|
@@ -3022,14 +3173,14 @@ var MesKernel = class extends FlowEngine {
|
|
|
3022
3173
|
const rc = this.recipeDef();
|
|
3023
3174
|
const ops = this.routeOps();
|
|
3024
3175
|
const i = ops.findIndex((s) => s.key === t.kind);
|
|
3025
|
-
const
|
|
3176
|
+
const loc = this.locations.get(t.toNode);
|
|
3026
3177
|
const bizStep = ops[i]?.bizStep ?? MES_BIZSTEP.producing;
|
|
3027
3178
|
const isLast = i === ops.length - 1;
|
|
3028
3179
|
if (!isLast) {
|
|
3029
3180
|
const inputs = order.allocated.slice();
|
|
3030
3181
|
const wip2 = sgtinUri(this.mesSpec.companyPrefix, "WIP", ++this.wipSeq);
|
|
3031
3182
|
const wipGtin = sgtinClass(this.mesSpec.companyPrefix, "WIP");
|
|
3032
|
-
this.transform(inputs, [{ epc: wip2, gtin: wipGtin, qty: 1, location:
|
|
3183
|
+
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
3184
|
order.allocated = [wip2];
|
|
3034
3185
|
const next = ops[i + 1];
|
|
3035
3186
|
this.emitStationDef(order, next, wip2);
|
|
@@ -3037,14 +3188,14 @@ var MesKernel = class extends FlowEngine {
|
|
|
3037
3188
|
this.emitOrder(order);
|
|
3038
3189
|
return;
|
|
3039
3190
|
}
|
|
3040
|
-
const fgStore = this.
|
|
3191
|
+
const fgStore = this.locationByType("fg-store");
|
|
3041
3192
|
const wip = order.allocated[0];
|
|
3042
3193
|
const good = this.rng() < (this.paramNumber(t.kind, OP_PARAM.yield) ?? DEFAULT_YIELD);
|
|
3043
3194
|
this.recordOutput(t.resource, good);
|
|
3044
3195
|
const disp = good ? DISP.sellable : DISP.non_sellable;
|
|
3045
3196
|
const outEpc = this.serialOf(rc.outputs[0].material, ++this.prodSeq);
|
|
3046
3197
|
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:
|
|
3198
|
+
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
3199
|
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
3200
|
order.allocated = [];
|
|
3050
3201
|
order.fulfilled = 1;
|
|
@@ -3052,6 +3203,43 @@ var MesKernel = class extends FlowEngine {
|
|
|
3052
3203
|
this.emitOrder(order);
|
|
3053
3204
|
}
|
|
3054
3205
|
};
|
|
3206
|
+
|
|
3207
|
+
// src/vocabulary.ts
|
|
3208
|
+
var RETIRED_VOCABULARY = ["mover", "Mover", "MOVER", "node", "Node", "NODE"];
|
|
3209
|
+
var VOCABULARY_EXCEPTIONS = [
|
|
3210
|
+
/* ── 저널 와이어 필드 — append-only 역사이므로 이름을 바꾸지 않는다 ───────
|
|
3211
|
+
* 같은 사실이 시점에 따라 다른 키로 들어가면 낡은 이름보다 나쁘다. 개명은 이벤트 스키마
|
|
3212
|
+
* 버저닝과 함께 다룬다. (개명 시점 실측: moverId 8,965행 · fromNode/toNode 9,679행) */
|
|
3213
|
+
{ token: "moverId", why: "journal wire field \u2014 renaming would mix two keys for one fact across history" },
|
|
3214
|
+
{ token: "fromNode", why: "journal wire field (task.status payload)" },
|
|
3215
|
+
{ token: "toNode", why: "journal wire field (task.status payload)" },
|
|
3216
|
+
/* ── 씬 컴포넌트 타입 — 보드에 저장된 값이고, 뜻이 어긋나지도 않는다 ──────
|
|
3217
|
+
* 보드 7개가 이 타입으로 컴포넌트를 담고 있어 개명하면 그 컴포넌트가 조용히 안 그려진다.
|
|
3218
|
+
* 그리고 씬에서 이 이름은 자원이 아니라 **움직임 표현**을 가리킨다(표준과 충돌 아님). */
|
|
3219
|
+
{ token: "twin-mover", why: "scene component type persisted in boards; names a motion representation, not a resource" },
|
|
3220
|
+
{ token: "TwinMover", why: "class backing the persisted scene component type above" },
|
|
3221
|
+
/* ── 플랫폼·언어 어휘 — 우리 도메인 낱말이 아니다 ─────────────────────── */
|
|
3222
|
+
{ token: "node_modules", why: "npm path" },
|
|
3223
|
+
{ token: "node:", why: "Node.js builtin module specifier" },
|
|
3224
|
+
{ token: "nodemon", why: "tool name" },
|
|
3225
|
+
{ token: "Node.js", why: "platform name" },
|
|
3226
|
+
{ token: "NodeJS", why: "TypeScript global namespace" },
|
|
3227
|
+
{ token: "nodeName", why: "DOM API" },
|
|
3228
|
+
{ token: "childNodes", why: "DOM API" },
|
|
3229
|
+
{ token: "appendChild", why: "DOM API (no match, kept for clarity)" }
|
|
3230
|
+
];
|
|
3231
|
+
var GUARD_PRAGMA = "vocabulary-guard: allow";
|
|
3232
|
+
function retiredVocabularyIn(line) {
|
|
3233
|
+
if (line.includes(GUARD_PRAGMA)) return [];
|
|
3234
|
+
let rest = line;
|
|
3235
|
+
for (const { token } of VOCABULARY_EXCEPTIONS) rest = rest.split(token).join(" ");
|
|
3236
|
+
const hits = [];
|
|
3237
|
+
for (const word of RETIRED_VOCABULARY) {
|
|
3238
|
+
const re = new RegExp(`[A-Za-z_]*${word}[A-Za-z_]*`, "g");
|
|
3239
|
+
for (const m of rest.matchAll(re)) hits.push(m[0]);
|
|
3240
|
+
}
|
|
3241
|
+
return hits;
|
|
3242
|
+
}
|
|
3055
3243
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3056
3244
|
0 && (module.exports = {
|
|
3057
3245
|
BIZSTEP,
|
|
@@ -3065,34 +3253,39 @@ var MesKernel = class extends FlowEngine {
|
|
|
3065
3253
|
DOMAIN_CATALOG,
|
|
3066
3254
|
DOMAIN_SYSTEMS,
|
|
3067
3255
|
EPCIS_CONTEXT,
|
|
3256
|
+
EQUIPMENT_LEVEL,
|
|
3068
3257
|
EventJournal,
|
|
3069
3258
|
FlowEngine,
|
|
3259
|
+
GUARD_PRAGMA,
|
|
3070
3260
|
ILMD_ATTR,
|
|
3261
|
+
LOCATION_SATURATION_NEAR,
|
|
3071
3262
|
MES_BIZSTEP,
|
|
3072
|
-
|
|
3263
|
+
MES_LOCATION_TYPES,
|
|
3073
3264
|
MES_PART_GTINS,
|
|
3074
3265
|
MES_PRODUCTS,
|
|
3075
3266
|
MES_PRODUCT_GTINS,
|
|
3076
3267
|
MES_TYPES,
|
|
3077
3268
|
MesKernel,
|
|
3078
|
-
NODE_SATURATION_NEAR,
|
|
3079
3269
|
OP_EVENT,
|
|
3080
3270
|
OP_PARAM,
|
|
3081
3271
|
ObservedReducer,
|
|
3272
|
+
RETIRED_VOCABULARY,
|
|
3082
3273
|
StateProjector,
|
|
3083
3274
|
TwinHistory,
|
|
3084
3275
|
TwinObserver,
|
|
3085
3276
|
TwinRuntime,
|
|
3086
3277
|
UTC_OFFSET,
|
|
3087
|
-
|
|
3278
|
+
VOCABULARY_EXCEPTIONS,
|
|
3279
|
+
WMS_LOCATION_TYPES,
|
|
3088
3280
|
WMS_TYPES,
|
|
3089
3281
|
WmsKernel,
|
|
3090
3282
|
YARD_BIZSTEP,
|
|
3091
|
-
|
|
3283
|
+
YMS_LOCATION_TYPES,
|
|
3092
3284
|
YMS_TYPES,
|
|
3093
3285
|
YmsKernel,
|
|
3094
3286
|
aggregationEvent,
|
|
3095
3287
|
capabilitiesForType,
|
|
3288
|
+
classClosure,
|
|
3096
3289
|
compareStates,
|
|
3097
3290
|
computeOee,
|
|
3098
3291
|
constantDuration,
|
|
@@ -3103,16 +3296,22 @@ var MesKernel = class extends FlowEngine {
|
|
|
3103
3296
|
foldTaskRecords,
|
|
3104
3297
|
gdtiUri,
|
|
3105
3298
|
graiUri,
|
|
3299
|
+
hierarchyOf,
|
|
3106
3300
|
ingest,
|
|
3301
|
+
isEquipmentLevel,
|
|
3107
3302
|
lgtinClass,
|
|
3303
|
+
locationStatusOf,
|
|
3108
3304
|
mapRecord,
|
|
3109
3305
|
monteCarloForecast,
|
|
3110
|
-
nodeStatusOf,
|
|
3111
3306
|
objectEvent,
|
|
3112
3307
|
parseEpc,
|
|
3113
3308
|
parseIsoDuration,
|
|
3114
3309
|
partialFitPolicy,
|
|
3310
|
+
readBoardAssets,
|
|
3311
|
+
readBoardEquipment,
|
|
3312
|
+
readBoardLocations,
|
|
3115
3313
|
replay,
|
|
3314
|
+
retiredVocabularyIn,
|
|
3116
3315
|
sgtinClass,
|
|
3117
3316
|
sgtinUri,
|
|
3118
3317
|
ssccUri,
|