@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/flow-engine.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* FlowEngine — 도메인-일반 flow 엔진 base (WMS·YMS 공유 mechanics 추출).
|
|
3
3
|
*
|
|
4
|
-
* WMS/YMS 커널의 mechanics(RNG·clock·scenario·tick
|
|
4
|
+
* WMS/YMS 커널의 mechanics(RNG·clock·scenario·tick 루프·설비 배정·태스크 진행·emit·snapshot·
|
|
5
5
|
* slotViews)는 동일했다 → 이 base 로 승격. 도메인 kernel 은 **flow 동사만** 구현:
|
|
6
6
|
* onArrival(자극:입고 도착) · onOrder(자극:출고 오더) · allocate(오더→태스크) · onTaskComplete(태스크 완료)
|
|
7
7
|
*
|
|
8
8
|
* TwinKernel 구현 → StateProjector·TwinRuntime 가 도메인 무관하게 소비.
|
|
9
9
|
* 통합 타입은 도메인 필드를 옵셔널로 넓혀(FlowItem.gtin?, FlowOrder.shipmentEpc? 등) 두 도메인을 담는다.
|
|
10
|
-
* (roadmap Phase5 발견 → 추출. [[project_flow_single_base_vision]]
|
|
10
|
+
* (roadmap Phase5 발견 → 추출. [[project_flow_single_base_vision]] FlowLocation 단일 base 방향과 정합.)
|
|
11
11
|
*/
|
|
12
|
-
import { OP_EVENT, CMD,
|
|
12
|
+
import { OP_EVENT, CMD, locationStatusOf, readBoardEquipment, readBoardLocations, readBoardAssets, classClosure } from "./contract.js";
|
|
13
13
|
import { ObservedReducer } from "./observed-reducer.js";
|
|
14
14
|
import { transformationEvent, aggregationEvent, objectEvent, parseEpc, DISP, ILMD_ATTR } from "./epcis.js";
|
|
15
15
|
import { parseIsoDuration } from "./iso-duration.js";
|
|
@@ -26,7 +26,7 @@ function mulberry32(seed) {
|
|
|
26
26
|
return fn;
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
* 주목신호 계산(순수) — State 스냅샷(
|
|
29
|
+
* 주목신호 계산(순수) — State 스냅샷(equipment/locations/orders)에서 attentions 파생.
|
|
30
30
|
* FlowEngine.computeAttentions(sim) 와 live projector 미러가 **공유**하는 계산 층(face2-inbound-live §1.1):
|
|
31
31
|
* sim/live 가 같은 임계값·규칙을 쓴다. good/scrap 미제공(관측 상태에 없음)이면 scrap-high 는 자연 스킵(metric 층 갭).
|
|
32
32
|
*/
|
|
@@ -34,12 +34,12 @@ export function deriveAttentions(view, acked) {
|
|
|
34
34
|
// 언어 중립: kind + params(원시값) + 조치 code 만 방출. 사람이 읽는 title/detail/rationale/라벨은
|
|
35
35
|
// 표현계층(클라 i18next)이 kind/code 로 렌더한다(무방언·다국어 — design/plans/i18n.md L3).
|
|
36
36
|
const out = [];
|
|
37
|
-
for (const m of view.
|
|
37
|
+
for (const m of view.equipment) {
|
|
38
38
|
if (m.status === 'down') {
|
|
39
39
|
out.push({
|
|
40
40
|
id: `breakdown:${m.id}`, kind: 'breakdown', severity: 'critical',
|
|
41
|
-
anchor: { moverId: m.id,
|
|
42
|
-
params: { moverId: m.id, ...(m.location ? {
|
|
41
|
+
anchor: { moverId: m.id, locationId: m.location },
|
|
42
|
+
params: { moverId: m.id, ...(m.location ? { locationId: m.location } : {}) },
|
|
43
43
|
recommendedActions: [
|
|
44
44
|
{ code: 'act.repair', command: CMD.resourceRepair, args: { resourceId: m.id } },
|
|
45
45
|
{ code: 'act.hold-until-repair', command: CMD.resourceHold, args: { resourceId: m.id } }
|
|
@@ -48,29 +48,29 @@ export function deriveAttentions(view, acked) {
|
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
for (const n of view.
|
|
51
|
+
for (const n of view.locations) {
|
|
52
52
|
if ((n.capacity ?? 0) > 0) {
|
|
53
53
|
const r = (n.occupancy ?? 0) / n.capacity;
|
|
54
54
|
if (r >= 0.9) {
|
|
55
55
|
const saturated = r >= 1;
|
|
56
56
|
out.push({
|
|
57
57
|
id: `bottleneck:${n.id}`, kind: 'bottleneck', severity: saturated ? 'high' : 'medium',
|
|
58
|
-
anchor: {
|
|
59
|
-
params: {
|
|
58
|
+
anchor: { locationId: n.id },
|
|
59
|
+
params: { locationId: n.id, occupancy: n.occupancy ?? 0, capacity: n.capacity ?? 0, ratioPct: Math.round(r * 100), saturated: saturated ? 1 : 0 },
|
|
60
60
|
recommendedActions: [{ code: 'advice.add-resource' }, { code: 'advice.downstream-priority' }]
|
|
61
61
|
// 권고만(command 없음) — 표현계층이 advice.* 코드로 렌더
|
|
62
62
|
});
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
-
for (const m of view.
|
|
66
|
+
for (const m of view.equipment) {
|
|
67
67
|
const total = (m.goodCount ?? 0) + (m.scrapCount ?? 0);
|
|
68
68
|
if (total >= 10) {
|
|
69
69
|
const rate = (m.scrapCount ?? 0) / total;
|
|
70
70
|
if (rate >= 0.15)
|
|
71
71
|
out.push({
|
|
72
72
|
id: `scrap:${m.id}`, kind: 'scrap-high', severity: rate >= 0.3 ? 'high' : 'medium',
|
|
73
|
-
anchor: { moverId: m.id,
|
|
73
|
+
anchor: { moverId: m.id, locationId: m.location },
|
|
74
74
|
params: { moverId: m.id, goodCount: m.goodCount ?? 0, scrapCount: m.scrapCount ?? 0, ratePct: Math.round(rate * 100) },
|
|
75
75
|
recommendedActions: [
|
|
76
76
|
{ code: 'act.hold-for-inspection', command: CMD.resourceHold, args: { resourceId: m.id } },
|
|
@@ -115,9 +115,11 @@ export function computeOee(c, nowMs) {
|
|
|
115
115
|
}
|
|
116
116
|
export class FlowEngine {
|
|
117
117
|
tenantId;
|
|
118
|
-
|
|
118
|
+
locations = new Map();
|
|
119
119
|
items = new Map();
|
|
120
|
-
|
|
120
|
+
equipment = new Map();
|
|
121
|
+
/** 등급 정의(표준 `<X>Class`) — 상속·유효기간 판정의 재료. 선언 안 하면 비어 있고, 소속 그대로 판정한다. */
|
|
122
|
+
classDefs = {};
|
|
121
123
|
/** 사람 — 선언하지 않으면 빈 맵(인원 제약 없는 트윈, 기존 거동). */
|
|
122
124
|
persons = new Map();
|
|
123
125
|
/** 물리 자산 — 선언하지 않으면 빈 맵(자산 제약 없는 트윈, 기존 거동). */
|
|
@@ -164,50 +166,51 @@ export class FlowEngine {
|
|
|
164
166
|
// ── TwinKernel (mechanics, 도메인 무관) ───────────────────────────────────
|
|
165
167
|
loadBoard(def) {
|
|
166
168
|
this.boardDef = def;
|
|
167
|
-
for (const n of def
|
|
168
|
-
this.
|
|
169
|
+
for (const n of readBoardLocations(def))
|
|
170
|
+
this.locations.set(n.id, { id: n.id, type: n.type, capacity: n.capacity, parallelism: n.parallelism, occupancy: 0, status: 'idle', parentId: n.parentId });
|
|
171
|
+
this.classDefs = { personnel: def.personnelClasses, equipment: def.equipmentClasses, asset: def.assetClasses };
|
|
169
172
|
for (const p of def.persons ?? [])
|
|
170
|
-
this.persons.set(p.id, { id: p.id,
|
|
171
|
-
for (const a of def
|
|
172
|
-
this.assets.set(a.id, { id: a.id,
|
|
173
|
-
for (const m of def
|
|
174
|
-
const
|
|
173
|
+
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 } : {}) });
|
|
174
|
+
for (const a of readBoardAssets(def))
|
|
175
|
+
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 } : {}) });
|
|
176
|
+
for (const m of readBoardEquipment(def)) {
|
|
177
|
+
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 };
|
|
175
178
|
if (m.mtbfMs !== undefined) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
+
eq.mtbfMs = m.mtbfMs;
|
|
180
|
+
eq.mttrMs = m.mttrMs;
|
|
181
|
+
eq.nextFailureMs = this.sampleExp(m.mtbfMs);
|
|
179
182
|
}
|
|
180
|
-
this.
|
|
183
|
+
this.equipment.set(m.id, eq);
|
|
181
184
|
}
|
|
182
185
|
}
|
|
183
186
|
/**
|
|
184
|
-
* what-if 구성 변주 — fork(또는 실행 중) 엔진에
|
|
187
|
+
* what-if 구성 변주 — fork(또는 실행 중) 엔진에 설비 추가. loadBoard 설비 삽입과 동일 규약.
|
|
185
188
|
* 기본은 mtbf 미지정(고장 없는 신뢰 자원) → sampleExp(rng) 무소비라 baseline fork 와 깨끗이 비교 가능.
|
|
186
189
|
*/
|
|
187
|
-
|
|
188
|
-
if (this.
|
|
190
|
+
addEquipment(m) {
|
|
191
|
+
if (this.equipment.has(m.id))
|
|
189
192
|
return;
|
|
190
|
-
const
|
|
193
|
+
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 };
|
|
191
194
|
if (m.mtbfMs !== undefined) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
+
eq.mtbfMs = m.mtbfMs;
|
|
196
|
+
eq.mttrMs = m.mttrMs;
|
|
197
|
+
eq.nextFailureMs = this.sampleExp(m.mtbfMs);
|
|
195
198
|
}
|
|
196
|
-
this.
|
|
199
|
+
this.equipment.set(m.id, eq);
|
|
197
200
|
}
|
|
198
201
|
/**
|
|
199
|
-
* 관측 상태 주입(라이브 예측용, kernel-unification P1) — 외부 관측 스냅샷(
|
|
202
|
+
* 관측 상태 주입(라이브 예측용, kernel-unification P1) — 외부 관측 스냅샷(재고·설비·자리)과
|
|
200
203
|
* 저널 오더(원값+라인)로 이 커널의 맵을 채운다. tick 으로 만든 게 아니라 "현재 관측된 현실"을 심어
|
|
201
204
|
* 이후 fork/tick 으로 예측한다. 라이브 런타임은 여전히 projector 미러 — 이 커널은 예측용 임시본.
|
|
202
205
|
* 오더는 남은 데맨드(라인별 requested-fulfilled)를 'created' 로 복원(현재 재고에서 재계획).
|
|
203
206
|
* 진행 중 개별 task 의 내부 상태는 관측만으론 복원 불가 → 재계획에 맡김(정직한 한계).
|
|
204
207
|
*/
|
|
205
208
|
hydrateObserved(snap, orders = []) {
|
|
206
|
-
/* **관측된 것을 버리지 않는다.** 예전에는
|
|
209
|
+
/* **관측된 것을 버리지 않는다.** 예전에는 자리·설비 상태를 'idle' 로, OEE 누적을 0 으로 덮고
|
|
207
210
|
* 물품의 로트·단위·소속·마스터데이터를 떨어뜨렸다. 씨앗이 잃은 것은 **예측도 모른다** —
|
|
208
211
|
* 고장 난 설비를 정상으로, 진행 중인 일을 없는 것으로 놓고 미래를 굴리면 답이 낙관 쪽으로 치우친다. */
|
|
209
|
-
for (const n of snap.
|
|
210
|
-
this.
|
|
212
|
+
for (const n of snap.locations) {
|
|
213
|
+
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 });
|
|
211
214
|
}
|
|
212
215
|
this.items.clear();
|
|
213
216
|
for (const it of snap.items) {
|
|
@@ -218,7 +221,7 @@ export class FlowEngine {
|
|
|
218
221
|
parent: it.parent, carriedBy: it.carriedBy, expiry: it.expiry, ilmd: it.ilmd
|
|
219
222
|
});
|
|
220
223
|
}
|
|
221
|
-
for (const m of snap.
|
|
224
|
+
for (const m of snap.equipment) {
|
|
222
225
|
/*
|
|
223
226
|
* 관측된 계측을 이어받는다 — **없어서 0 이었던 게 아니라 잘못 읽어서 0 이었다.**
|
|
224
227
|
* `OeeMetrics` 의 이름은 `runMs`·`setupMs`·`downMs`·`goodCount`·`scrapCount` 인데 예전 코드가
|
|
@@ -231,8 +234,8 @@ export class FlowEngine {
|
|
|
231
234
|
* 값을 그대로 이어받고, 없으면 0 에서 시작한다(꾸미지 않는다).
|
|
232
235
|
*/
|
|
233
236
|
const oee = m.oee;
|
|
234
|
-
this.
|
|
235
|
-
id: m.id, kind: m.kind, location: m.location ?? '', status: m.status ?? 'idle', taskId: null,
|
|
237
|
+
this.equipment.set(m.id, {
|
|
238
|
+
id: m.id, kind: m.kind, location: m.location ?? '', ...(m.homeLocation ? { homeLocation: m.homeLocation } : {}), status: m.status ?? 'idle', taskId: null,
|
|
236
239
|
runMs: oee?.runMs ?? 0, setupMs: oee?.setupMs ?? 0, downMs: oee?.downMs ?? 0,
|
|
237
240
|
goodCount: oee?.goodCount ?? 0, scrapCount: oee?.scrapCount ?? 0
|
|
238
241
|
});
|
|
@@ -241,11 +244,11 @@ export class FlowEngine {
|
|
|
241
244
|
* 씨앗이 버리면, 인원을 요구하는 공정의 작업이 하나도 시작되지 못하고 예측이 멈춘다(0 명 < 2 명).
|
|
242
245
|
* 배정 상태(busy/taskId)는 아래 작업 복원이 다시 세우므로 여기서는 등급·교대만 살린다. */
|
|
243
246
|
for (const p of snap.persons ?? []) {
|
|
244
|
-
this.persons.set(p.id, { id: p.id,
|
|
247
|
+
this.persons.set(p.id, { id: p.id, personnelClassIds: p.personnelClassIds, status: 'idle', taskId: null });
|
|
245
248
|
}
|
|
246
249
|
/* 자산도 같다 — 잃으면 자산을 요구하는 작업이 영원히 못 나간다(빈 팔레트 0개 < 1개). */
|
|
247
250
|
for (const a of snap.assets ?? []) {
|
|
248
|
-
this.assets.set(a.id, { id: a.id,
|
|
251
|
+
this.assets.set(a.id, { id: a.id, assetClassIds: a.assetClassIds, location: a.location, status: 'idle', taskId: null, carrying: a.carrying });
|
|
249
252
|
}
|
|
250
253
|
/* 진행 중이던 작업을 이어 붙인다 — 없으면 예측이 "일이 하나도 없는 현장" 에서 출발한다.
|
|
251
254
|
* 남은 시간을 모르면 **진척을 꾸미지 않고** 미착수(created)로 되돌린다: 그 일이 남아 있다는 사실은
|
|
@@ -268,7 +271,7 @@ export class FlowEngine {
|
|
|
268
271
|
});
|
|
269
272
|
/* 진행 중으로 살린 작업은 그 자원을 점유한 상태여야 한다(자원이 동시에 다른 일을 받지 않게). */
|
|
270
273
|
if (known && t.status === 'in-progress' && t.resourceRef) {
|
|
271
|
-
const mv = this.
|
|
274
|
+
const mv = this.equipment.get(t.resourceRef);
|
|
272
275
|
if (mv) {
|
|
273
276
|
mv.status = 'busy';
|
|
274
277
|
mv.taskId = t.id;
|
|
@@ -303,9 +306,9 @@ export class FlowEngine {
|
|
|
303
306
|
this.orders.set(o.orderId, { id: o.orderId, kind: o.kind, status: 'created', requested: remaining, fulfilled: 0, bizTransaction: '', allocated: [], picked: [], shipmentEpc: null, lines });
|
|
304
307
|
}
|
|
305
308
|
}
|
|
306
|
-
/** what-if 구성 변주 —
|
|
307
|
-
|
|
308
|
-
const n = this.
|
|
309
|
+
/** what-if 구성 변주 — 자리 용량 변경(fork 대상). 존재하면 true. */
|
|
310
|
+
setLocationCapacity(locationId, capacity) {
|
|
311
|
+
const n = this.locations.get(locationId);
|
|
309
312
|
if (!n)
|
|
310
313
|
return false;
|
|
311
314
|
n.capacity = Math.max(0, capacity);
|
|
@@ -351,30 +354,30 @@ export class FlowEngine {
|
|
|
351
354
|
this._acked.add(id);
|
|
352
355
|
return ok();
|
|
353
356
|
}
|
|
354
|
-
// Operable 코어 — 자원(
|
|
357
|
+
// Operable 코어 — 자원(설비·설비) 제어. capability-keyed(resourceId), 모든 operable 자원 공통.
|
|
355
358
|
case CMD.resourceHold:
|
|
356
359
|
case CMD.resourceResume: {
|
|
357
|
-
const m = this.
|
|
360
|
+
const m = this.equipment.get(cmd.args?.resourceId ?? '');
|
|
358
361
|
if (!m)
|
|
359
362
|
return fail('resource-not-found');
|
|
360
363
|
m.held = cmd.type === CMD.resourceHold; // 계획 정지 → 배정 스킵
|
|
361
|
-
this.
|
|
364
|
+
this.emitEquipment(m);
|
|
362
365
|
return ok();
|
|
363
366
|
}
|
|
364
367
|
case CMD.resourceDown: {
|
|
365
368
|
const a = cmd.args;
|
|
366
|
-
const m = this.
|
|
369
|
+
const m = this.equipment.get(a?.resourceId ?? '');
|
|
367
370
|
if (!m)
|
|
368
371
|
return fail('resource-not-found');
|
|
369
372
|
if (m.status !== 'down') {
|
|
370
373
|
m.status = 'down'; // 비계획 고장 주입 — 기존 고장 machinery(processFailures)가 downMs 누적·수리 처리
|
|
371
374
|
m.repairUntilMs = this.clockMs + (Number(a?.durationMs) || m.mttrMs || 60_000);
|
|
372
|
-
this.
|
|
375
|
+
this.emitEquipment(m);
|
|
373
376
|
}
|
|
374
377
|
return ok();
|
|
375
378
|
}
|
|
376
379
|
case CMD.resourceRepair: {
|
|
377
|
-
const m = this.
|
|
380
|
+
const m = this.equipment.get(cmd.args?.resourceId ?? '');
|
|
378
381
|
if (!m)
|
|
379
382
|
return fail('resource-not-found');
|
|
380
383
|
if (m.status === 'down') {
|
|
@@ -382,12 +385,12 @@ export class FlowEngine {
|
|
|
382
385
|
m.repairUntilMs = undefined;
|
|
383
386
|
if (m.mtbfMs !== undefined)
|
|
384
387
|
m.nextFailureMs = this.clockMs + this.sampleExp(m.mtbfMs);
|
|
385
|
-
this.
|
|
388
|
+
this.emitEquipment(m);
|
|
386
389
|
}
|
|
387
390
|
return ok();
|
|
388
391
|
}
|
|
389
392
|
case CMD.resourceResetMetrics: {
|
|
390
|
-
const m = this.
|
|
393
|
+
const m = this.equipment.get(cmd.args?.resourceId ?? '');
|
|
391
394
|
if (!m)
|
|
392
395
|
return fail('resource-not-found');
|
|
393
396
|
m.runMs = 0;
|
|
@@ -397,27 +400,27 @@ export class FlowEngine {
|
|
|
397
400
|
m.scrapCount = 0;
|
|
398
401
|
m.holdMs = 0;
|
|
399
402
|
m.metricsSinceMs = this.clockMs; // 계측 창을 지금부터 재시작
|
|
400
|
-
this.
|
|
403
|
+
this.emitEquipment(m);
|
|
401
404
|
return ok();
|
|
402
405
|
}
|
|
403
406
|
case CMD.resourceAdd: {
|
|
404
|
-
// 라이브 자원 추가(실제 act) —
|
|
405
|
-
// 새
|
|
407
|
+
// 라이브 자원 추가(실제 act) — addEquipment(what-if 와 동일 경로)로 런타임에 설비 삽입 + equipment 델타 방출.
|
|
408
|
+
// 새 설비는 즉시 배정 대상(다음 tick). 유일 id 생성(충돌 회피). 좌표/persistence 는 호스트 몫(커널=위상만).
|
|
406
409
|
const a = cmd.args;
|
|
407
410
|
if (!a?.kind)
|
|
408
411
|
return fail('kind-required');
|
|
409
|
-
if (!a?.
|
|
410
|
-
return fail('home-
|
|
412
|
+
if (!a?.homeLocation || !this.locations.has(a.homeLocation))
|
|
413
|
+
return fail('home-location-not-found', { homeLocation: a?.homeLocation ?? '' });
|
|
411
414
|
const count = Math.max(1, Math.min(50, Number(a.count) || 1));
|
|
412
|
-
let seq = this.
|
|
415
|
+
let seq = this.equipment.size;
|
|
413
416
|
for (let i = 0; i < count; i++) {
|
|
414
417
|
let id = `${a.kind}-${++seq}`;
|
|
415
|
-
while (this.
|
|
418
|
+
while (this.equipment.has(id))
|
|
416
419
|
id = `${a.kind}-${++seq}`;
|
|
417
|
-
this.
|
|
418
|
-
const m = this.
|
|
420
|
+
this.addEquipment({ id, kind: a.kind, homeLocation: a.homeLocation });
|
|
421
|
+
const m = this.equipment.get(id);
|
|
419
422
|
if (m)
|
|
420
|
-
this.
|
|
423
|
+
this.emitEquipment(m); // equipment.status 델타 → 상태/저널에 새 설비 반영
|
|
421
424
|
}
|
|
422
425
|
return ok();
|
|
423
426
|
}
|
|
@@ -462,28 +465,38 @@ export class FlowEngine {
|
|
|
462
465
|
simClockMs: this.clockMs,
|
|
463
466
|
/* 출처 표시 — 보드(마스터)에서 온 자리다. 미러는 관측으로 알게 된 자리를 'observed' 로 구별하는데,
|
|
464
467
|
* 시뮬이 아무 표시도 안 하면 소비처가 두 스냅샷을 같은 규칙으로 읽지 못한다. */
|
|
465
|
-
|
|
468
|
+
locations: [...this.locations.values()].map(n => {
|
|
466
469
|
const { status, ...rest } = n;
|
|
467
470
|
/* 상태는 저장값이 아니라 포화도 파생 — 미러와 **같은 함수**를 쓴다(규칙이 둘이면 갈라진다). */
|
|
468
|
-
const derived =
|
|
471
|
+
const derived = locationStatusOf(n);
|
|
469
472
|
return { ...rest, ...(derived ? { status: derived } : {}), origin: 'master' };
|
|
470
473
|
}),
|
|
471
474
|
items: [...this.items.values()].map(i => this.itemState(i)),
|
|
472
|
-
|
|
473
|
-
const s = { id: m.id, kind: m.kind, location: m.location, status: m.status, taskId: m.taskId ?? undefined, oee: this.oeeOf(m), held: m.held, origin: 'master', ...(this.offShift(m) ? { offShift: true } : {}) };
|
|
475
|
+
equipment: [...this.equipment.values()].map(m => {
|
|
476
|
+
const s = { id: m.id, kind: m.kind, location: m.location, ...(m.homeLocation ? { homeLocation: m.homeLocation } : {}), status: m.status, taskId: m.taskId ?? undefined, oee: this.oeeOf(m), held: m.held, ...(m.properties ? { properties: m.properties } : {}), ...(m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}), origin: 'master', ...(this.offShift(m) ? { offShift: true } : {}) };
|
|
474
477
|
const t = m.taskId ? this.tasks.get(m.taskId) : undefined;
|
|
475
478
|
if (t && t.status === 'in-progress' && t.intent !== 'process')
|
|
476
479
|
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 };
|
|
477
480
|
return s;
|
|
478
481
|
}),
|
|
479
482
|
assets: [...this.assets.values()].map(a => {
|
|
480
|
-
const st = { id: a.id,
|
|
483
|
+
const st = { id: a.id, assetClassIds: a.assetClassIds, location: a.location, status: a.status, taskId: a.taskId ?? undefined };
|
|
481
484
|
if (a.carrying)
|
|
482
485
|
st.carrying = a.carrying;
|
|
486
|
+
if (a.properties)
|
|
487
|
+
st.properties = a.properties;
|
|
488
|
+
if (a.testSpecificationIds)
|
|
489
|
+
st.testSpecificationIds = a.testSpecificationIds;
|
|
483
490
|
return st;
|
|
484
491
|
}),
|
|
485
492
|
persons: [...this.persons.values()].map(p => {
|
|
486
|
-
const st = { id: p.id,
|
|
493
|
+
const st = { id: p.id, personnelClassIds: p.personnelClassIds, status: p.status, taskId: p.taskId ?? undefined };
|
|
494
|
+
if (p.location)
|
|
495
|
+
st.location = p.location;
|
|
496
|
+
if (p.properties)
|
|
497
|
+
st.properties = p.properties;
|
|
498
|
+
if (p.testSpecificationIds)
|
|
499
|
+
st.testSpecificationIds = p.testSpecificationIds;
|
|
487
500
|
if (this.personOffShift(p))
|
|
488
501
|
st.offShift = true;
|
|
489
502
|
return st;
|
|
@@ -512,13 +525,13 @@ export class FlowEngine {
|
|
|
512
525
|
};
|
|
513
526
|
}
|
|
514
527
|
/*
|
|
515
|
-
* 주목 신호 판단 — 상태(
|
|
528
|
+
* 주목 신호 판단 — 상태(자리·설비·오더)에서 도메인 조건을 평가해 Attention 방출.
|
|
516
529
|
* severity=ISA-18.2 우선순위 계열, kind=도메인 라벨. UI 는 판단 안 함(임계값 여기 소유).
|
|
517
530
|
* 도메인별 추가 판단은 서브클래스가 override 로 확장(super.computeAttentions() 합성).
|
|
518
531
|
*/
|
|
519
532
|
computeAttentions() {
|
|
520
533
|
// 계산 층은 순수 함수 deriveAttentions 로 위임 — sim(여기)과 live projector 미러가 공유(face2-inbound-live §1.1).
|
|
521
|
-
const out = deriveAttentions({
|
|
534
|
+
const out = deriveAttentions({ equipment: [...this.equipment.values()], locations: [...this.locations.values()], orders: [...this.orders.values()] }, this._acked);
|
|
522
535
|
// 확인(ack) 프루닝 — 사라진 조건은 ack 해제(재발 시 다시 active). 엔진 상태 변이라 여기 유지(marking 은 deriveAttentions).
|
|
523
536
|
const present = new Set(out.map(a => a.id));
|
|
524
537
|
for (const id of [...this._acked])
|
|
@@ -577,7 +590,7 @@ export class FlowEngine {
|
|
|
577
590
|
*/
|
|
578
591
|
apply(envelope) {
|
|
579
592
|
if (!this.observer) {
|
|
580
|
-
this.observer = new ObservedReducer(this.boardDef ?? {
|
|
593
|
+
this.observer = new ObservedReducer(this.boardDef ?? { locations: [], equipment: [] });
|
|
581
594
|
this.observeMode = true;
|
|
582
595
|
}
|
|
583
596
|
this.observer.apply(envelope);
|
|
@@ -754,8 +767,8 @@ export class FlowEngine {
|
|
|
754
767
|
}
|
|
755
768
|
return mix[mix.length - 1].gtin;
|
|
756
769
|
}
|
|
757
|
-
|
|
758
|
-
for (const n of this.
|
|
770
|
+
locationByType(type) {
|
|
771
|
+
for (const n of this.locations.values())
|
|
759
772
|
if (n.type === type)
|
|
760
773
|
return n;
|
|
761
774
|
return undefined;
|
|
@@ -764,7 +777,7 @@ export class FlowEngine {
|
|
|
764
777
|
* process 변화 대수(EPCIS TransformationEvent · ISA-95 Material Consumed/Produced · 씬 Processable.transform).
|
|
765
778
|
* inputs 소비 → outputs 생산. 입출력 arity 가 곧 대수:
|
|
766
779
|
* merge N→1(조립) · split 1→N(분해) · transform 1→1(타입변경) · loss N→0(소실) · gain 0→N(부산물·생성).
|
|
767
|
-
* 아이템 상태(소비/생산)와
|
|
780
|
+
* 아이템 상태(소비/생산)와 자리 점유를 갱신하고 계보(TransformationEvent)를 방출한다.
|
|
768
781
|
* 도메인은 이 원시만 호출 — onTaskComplete 의 소비/생산/EPCIS 손코딩을 대체.
|
|
769
782
|
*/
|
|
770
783
|
transform(inputs, outputs, opts) {
|
|
@@ -772,14 +785,14 @@ export class FlowEngine {
|
|
|
772
785
|
const it = this.items.get(epc);
|
|
773
786
|
if (!it)
|
|
774
787
|
continue;
|
|
775
|
-
const n = this.
|
|
788
|
+
const n = this.locations.get(it.location);
|
|
776
789
|
if (n)
|
|
777
790
|
n.occupancy--;
|
|
778
791
|
this.items.delete(epc);
|
|
779
792
|
}
|
|
780
793
|
for (const o of outputs) {
|
|
781
794
|
this.items.set(o.epc, { epc: o.epc, gtin: o.gtin, qty: o.qty, location: o.location, disposition: o.disposition });
|
|
782
|
-
const n = this.
|
|
795
|
+
const n = this.locations.get(o.location);
|
|
783
796
|
if (n)
|
|
784
797
|
n.occupancy++;
|
|
785
798
|
}
|
|
@@ -849,7 +862,7 @@ export class FlowEngine {
|
|
|
849
862
|
for (const c of children) {
|
|
850
863
|
const it = this.items.get(c);
|
|
851
864
|
if (it) {
|
|
852
|
-
const n = this.
|
|
865
|
+
const n = this.locations.get(it.location);
|
|
853
866
|
if (n)
|
|
854
867
|
n.occupancy--;
|
|
855
868
|
this.items.delete(c);
|
|
@@ -867,7 +880,7 @@ export class FlowEngine {
|
|
|
867
880
|
}
|
|
868
881
|
/**
|
|
869
882
|
* containment 분해(EPCIS AggregationEvent DELETE) — 부모(용기)에서 자식들을 풀어냄.
|
|
870
|
-
* materialize 지정 시 자식이 독립 아이템으로 등장(materialize:
|
|
883
|
+
* materialize 지정 시 자식이 독립 아이템으로 등장(materialize: 자리 배치 + occupancy + ObjectEvent ADD).
|
|
871
884
|
* 미지정 시 이벤트만.
|
|
872
885
|
*/
|
|
873
886
|
disaggregate(parent, children, opts) {
|
|
@@ -882,7 +895,7 @@ export class FlowEngine {
|
|
|
882
895
|
const m = opts.materialize;
|
|
883
896
|
for (const c of children) {
|
|
884
897
|
this.items.set(c, { epc: c, location: m.location, disposition: m.disposition });
|
|
885
|
-
const n = this.
|
|
898
|
+
const n = this.locations.get(m.location);
|
|
886
899
|
if (n)
|
|
887
900
|
n.occupancy++;
|
|
888
901
|
this.emit(objectEvent({ eventTime: this.now(), action: 'ADD', bizStep: opts.bizStep, disposition: m.disposition, epcList: [c], readPoint: m.location, bizLocation: m.location }));
|
|
@@ -891,7 +904,7 @@ export class FlowEngine {
|
|
|
891
904
|
}
|
|
892
905
|
/** 품질 보고(OEE Quality) — 도메인이 완료 시 자원별 양품/불량 1건 계상(예: 용접 수율). */
|
|
893
906
|
recordOutput(moverId, good) {
|
|
894
|
-
const m = moverId ? this.
|
|
907
|
+
const m = moverId ? this.equipment.get(moverId) : undefined;
|
|
895
908
|
if (!m)
|
|
896
909
|
return;
|
|
897
910
|
if (good)
|
|
@@ -901,20 +914,20 @@ export class FlowEngine {
|
|
|
901
914
|
// 품질 델타 방출 — live OEE 누적기가 good/scrap 을 정확 추적(equipment.status 는 quality 미포함). WMS/YMS 는 미호출→무영향.
|
|
902
915
|
this.emitOp(OP_EVENT.quality, { moverId: m.id, good, goodCount: m.goodCount, scrapCount: m.scrapCount });
|
|
903
916
|
}
|
|
904
|
-
/**
|
|
917
|
+
/** 설비 OEE(스냅샷 파생) — Availability×Performance×Quality. planned = 설비 존재 sim 시간(clockMs). */
|
|
905
918
|
oeeOf(m) {
|
|
906
919
|
// 순수 공식 computeOee 로 위임 — sim(여기)과 live 가 같은 계산 층 공유. planned 는 hold 제외(계획정지 OEE 무영향).
|
|
907
920
|
return computeOee(m, this.clockMs);
|
|
908
921
|
}
|
|
909
|
-
/** 정책에 넘길 특정 타입
|
|
910
|
-
slotViews(
|
|
922
|
+
/** 정책에 넘길 특정 타입 자리의 관측 뷰 — 예약(그 자리로 향하는 in-flight task) 포함. */
|
|
923
|
+
slotViews(locationType) {
|
|
911
924
|
const reserved = new Map();
|
|
912
925
|
for (const t of this.tasks.values())
|
|
913
926
|
if (t.status !== 'completed')
|
|
914
927
|
reserved.set(t.toNode, (reserved.get(t.toNode) ?? 0) + 1);
|
|
915
928
|
const views = [];
|
|
916
|
-
for (const n of this.
|
|
917
|
-
if (n.type ===
|
|
929
|
+
for (const n of this.locations.values())
|
|
930
|
+
if (n.type === locationType)
|
|
918
931
|
views.push({ id: n.id, capacity: n.capacity, occupancy: n.occupancy, reserved: reserved.get(n.id) ?? 0 });
|
|
919
932
|
return views;
|
|
920
933
|
}
|
|
@@ -953,15 +966,15 @@ export class FlowEngine {
|
|
|
953
966
|
}
|
|
954
967
|
/** 사람 상태 전이 — 설비와 별개 채널(어휘가 다르다: 고장이 아니라 교대·투입). */
|
|
955
968
|
emitAsset(a) {
|
|
956
|
-
this.emitOp(OP_EVENT.asset, { assetId: a.id,
|
|
969
|
+
this.emitOp(OP_EVENT.asset, { assetId: a.id, assetClassIds: a.assetClassIds, status: a.status, location: a.location, taskId: a.taskId ?? undefined, carrying: a.carrying });
|
|
957
970
|
}
|
|
958
971
|
emitPerson(p) {
|
|
959
|
-
this.emitOp(OP_EVENT.person, { personId: p.id,
|
|
972
|
+
this.emitOp(OP_EVENT.person, { personId: p.id, personnelClassIds: p.personnelClassIds, status: p.status, taskId: p.taskId ?? undefined, ...(p.location ? { location: p.location } : {}), ...(this.personOffShift(p) ? { offShift: true } : {}) });
|
|
960
973
|
}
|
|
961
974
|
/** 설비 상태 전이 — `taskId` 를 함께 싣는다(사람·자산 델타와 같은 규칙). 없으면 미러가 "이 설비가
|
|
962
975
|
* 무슨 일을 하는 중인가" 를 알 수 없어 작업↔자원 연결이 한쪽에서만 성립한다. */
|
|
963
|
-
|
|
964
|
-
this.emitOp(OP_EVENT.equipment, { moverId: m.id, kind: m.kind, status: m.status, location: m.location, taskId: m.taskId ?? undefined, motion });
|
|
976
|
+
emitEquipment(m, motion) {
|
|
977
|
+
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 ?? undefined, motion });
|
|
965
978
|
}
|
|
966
979
|
/** 오더 델타 — **라인까지 싣는다.** 라인이 빠지면 미러가 남은 데맨드를 라인별로 재계획할 수 없다. */
|
|
967
980
|
emitOrder(o) {
|
|
@@ -1041,7 +1054,12 @@ export class FlowEngine {
|
|
|
1041
1054
|
const want = Math.max(0, Math.floor(req.quantity ?? 0));
|
|
1042
1055
|
if (!want)
|
|
1043
1056
|
continue;
|
|
1044
|
-
const avail = [...this.assets.values()].filter(
|
|
1057
|
+
const avail = [...this.assets.values()].filter(
|
|
1058
|
+
/* 인원과 같은 규칙 — 상속을 타고 닫아 판정하고 유효기간 밖은 제외한다. 요구는 등급 하나+수량. */
|
|
1059
|
+
a => a.status === 'idle' &&
|
|
1060
|
+
!picked.includes(a.id) &&
|
|
1061
|
+
(req.assetClass === undefined ||
|
|
1062
|
+
classClosure(a.assetClassIds, this.classDefs.asset, this.now()).has(req.assetClass)));
|
|
1045
1063
|
if (avail.length < want)
|
|
1046
1064
|
return null;
|
|
1047
1065
|
for (let i = 0; i < want; i++)
|
|
@@ -1110,7 +1128,11 @@ export class FlowEngine {
|
|
|
1110
1128
|
const avail = [...this.persons.values()].filter(p => p.status === 'idle' &&
|
|
1111
1129
|
!picked.includes(p.id) &&
|
|
1112
1130
|
!this.personOffShift(p) &&
|
|
1113
|
-
|
|
1131
|
+
/* 자격은 **상속을 타고 닫아** 판정한다 — 사람은 여러 등급에 속할 수 있고(표준), 등급은
|
|
1132
|
+
다른 등급을 상속한다. "생산직 2명" 요구를 "용접 자격자" 가 만족해야 한다.
|
|
1133
|
+
유효기간 밖의 등급은 닫힘에서 빠진다(만료된 자격으로 배정되지 않는다). */
|
|
1134
|
+
(req.personnelClass === undefined ||
|
|
1135
|
+
classClosure(p.personnelClassIds, this.classDefs.personnel, this.now()).has(req.personnelClass)));
|
|
1114
1136
|
if (avail.length < want)
|
|
1115
1137
|
return null; // 한 등급이라도 모자라면 시작하지 않는다
|
|
1116
1138
|
for (let i = 0; i < want; i++)
|
|
@@ -1124,7 +1146,14 @@ export class FlowEngine {
|
|
|
1124
1146
|
* 여기서는 고르기만 한다 — 확정은 호출부가 다른 자원까지 확보한 뒤에 한다.
|
|
1125
1147
|
*/
|
|
1126
1148
|
claimEquipment(t) {
|
|
1127
|
-
const free = (kind, picked = []) => [...this.
|
|
1149
|
+
const free = (kind, picked = []) => [...this.equipment.values()].filter(m => m.status === 'idle' &&
|
|
1150
|
+
!m.held &&
|
|
1151
|
+
!this.offShift(m) &&
|
|
1152
|
+
!picked.includes(m.id) &&
|
|
1153
|
+
/* 설비의 소속은 아직 단수(`kind`)다 — 저널에 기록이 쌓여 복수화를 별도 작업으로 두었다.
|
|
1154
|
+
다만 등급 정의가 있으면 **상속은 지금도 탄다**: 'welder-6axis' 가 'welder' 를 상속하면
|
|
1155
|
+
'welder' 요구를 만족한다. 단수/복수와 상속은 다른 축이므로 하나를 기다리지 않는다. */
|
|
1156
|
+
(kind === undefined || classClosure(m.kind ? [m.kind] : [], this.classDefs.equipment, this.now()).has(kind)));
|
|
1128
1157
|
const need = this.operationSpecs.get(t.kind)?.equipmentSpecification;
|
|
1129
1158
|
if (!need?.length) {
|
|
1130
1159
|
const one = free(t.resourceType)[0];
|
|
@@ -1180,15 +1209,15 @@ export class FlowEngine {
|
|
|
1180
1209
|
* 이 자리가 동시 처리 한도에 찼는가 — `parallelism` 을 선언한 자리만 판정한다(미선언=제약 없음).
|
|
1181
1210
|
* 세는 대상은 **그 자리에서 진행 중인 작업**(`toNode` 기준, in-progress). 대기 중인 작업은 세지 않는다.
|
|
1182
1211
|
*/
|
|
1183
|
-
stationFull(
|
|
1184
|
-
if (!
|
|
1212
|
+
stationFull(locationId) {
|
|
1213
|
+
if (!locationId)
|
|
1185
1214
|
return false;
|
|
1186
|
-
const limit = this.
|
|
1215
|
+
const limit = this.locations.get(locationId)?.parallelism;
|
|
1187
1216
|
if (!(typeof limit === 'number' && limit > 0))
|
|
1188
1217
|
return false;
|
|
1189
1218
|
let running = 0;
|
|
1190
1219
|
for (const t of this.tasks.values())
|
|
1191
|
-
if (t.status === 'in-progress' && t.toNode ===
|
|
1220
|
+
if (t.status === 'in-progress' && t.toNode === locationId)
|
|
1192
1221
|
running++;
|
|
1193
1222
|
return running >= limit;
|
|
1194
1223
|
}
|
|
@@ -1219,12 +1248,12 @@ export class FlowEngine {
|
|
|
1219
1248
|
/** 지수분포 표본(고장/수리 간격) — mean 을 평균으로 하는 무기억 프로세스. */
|
|
1220
1249
|
sampleExp(meanMs) { return -Math.log(1 - this.rng()) * meanMs; }
|
|
1221
1250
|
/**
|
|
1222
|
-
* 확률적 설비 고장(MTBF/MTTR) — mtbf 지정
|
|
1251
|
+
* 확률적 설비 고장(MTBF/MTTR) — mtbf 지정 설비만 참여(미지정=고장 없음, rng 무소비 → byte-identical).
|
|
1223
1252
|
* up: nextFailure 도래 시 down(수리까지 repairUntil). down: downMs 누적, repair 도래 시 up(다음 고장 예약).
|
|
1224
|
-
* down 중
|
|
1253
|
+
* down 중 설비는 배정 불가 + 진행중 task 동결(processTasks 가 skip) → OEE Availability 손실.
|
|
1225
1254
|
*/
|
|
1226
1255
|
processFailures(dt) {
|
|
1227
|
-
for (const m of this.
|
|
1256
|
+
for (const m of this.equipment.values()) {
|
|
1228
1257
|
if (m.status === 'down') {
|
|
1229
1258
|
// down 회계 + 수리 — 확률적 고장·강제 고장(resource.down) 공통. repairUntilMs 도래 시 복구.
|
|
1230
1259
|
m.downMs += dt;
|
|
@@ -1233,7 +1262,7 @@ export class FlowEngine {
|
|
|
1233
1262
|
m.repairUntilMs = undefined;
|
|
1234
1263
|
if (m.mtbfMs !== undefined)
|
|
1235
1264
|
m.nextFailureMs = this.clockMs + this.sampleExp(m.mtbfMs);
|
|
1236
|
-
this.
|
|
1265
|
+
this.emitEquipment(m);
|
|
1237
1266
|
}
|
|
1238
1267
|
continue;
|
|
1239
1268
|
}
|
|
@@ -1243,12 +1272,12 @@ export class FlowEngine {
|
|
|
1243
1272
|
m.holdMs = (m.holdMs ?? 0) + dt;
|
|
1244
1273
|
continue;
|
|
1245
1274
|
}
|
|
1246
|
-
// 확률적 고장 — mtbf 지정
|
|
1275
|
+
// 확률적 고장 — mtbf 지정 설비만(미지정=고장 없음, rng 무소비 → byte-identical baseline).
|
|
1247
1276
|
if (m.mtbfMs !== undefined && this.clockMs >= (m.nextFailureMs ?? Infinity)) {
|
|
1248
1277
|
m.status = 'down';
|
|
1249
1278
|
m.repairUntilMs = this.clockMs + this.sampleExp(m.mttrMs ?? m.mtbfMs);
|
|
1250
1279
|
m.nextFailureMs = undefined;
|
|
1251
|
-
this.
|
|
1280
|
+
this.emitEquipment(m);
|
|
1252
1281
|
}
|
|
1253
1282
|
}
|
|
1254
1283
|
}
|
|
@@ -1356,21 +1385,21 @@ export class FlowEngine {
|
|
|
1356
1385
|
const rigs = this.claimEquipment(t);
|
|
1357
1386
|
if (!rigs)
|
|
1358
1387
|
continue;
|
|
1359
|
-
const
|
|
1360
|
-
// 체인지오버: task 의 changeoverKey 가
|
|
1361
|
-
if (t.setupMs && t.changeoverKey !== undefined &&
|
|
1388
|
+
const eq = this.equipment.get(rigs[0]);
|
|
1389
|
+
// 체인지오버: task 의 changeoverKey 가 설비 직전 키와 다르면 셋업 부착(첫 작업은 셋업 없음).
|
|
1390
|
+
if (t.setupMs && t.changeoverKey !== undefined && eq.lastChangeoverKey !== undefined && eq.lastChangeoverKey !== t.changeoverKey) {
|
|
1362
1391
|
t.appliedSetupMs = t.setupMs;
|
|
1363
|
-
t.durationMs += t.setupMs; // 셋업을 가동 앞에 folded(
|
|
1392
|
+
t.durationMs += t.setupMs; // 셋업을 가동 앞에 folded(설비 점유 = 셋업+사이클)
|
|
1364
1393
|
}
|
|
1365
1394
|
if (t.changeoverKey !== undefined)
|
|
1366
|
-
|
|
1395
|
+
eq.lastChangeoverKey = t.changeoverKey;
|
|
1367
1396
|
for (const id of rigs) {
|
|
1368
|
-
const m = this.
|
|
1397
|
+
const m = this.equipment.get(id);
|
|
1369
1398
|
m.status = 'busy';
|
|
1370
1399
|
m.taskId = t.id;
|
|
1371
1400
|
}
|
|
1372
1401
|
t.status = 'in-progress';
|
|
1373
|
-
t.resource =
|
|
1402
|
+
t.resource = eq.id;
|
|
1374
1403
|
t.remainingMs = t.durationMs;
|
|
1375
1404
|
t.startedAtSimMs = this.clockMs;
|
|
1376
1405
|
if (rigs.length > 1)
|
|
@@ -1380,9 +1409,9 @@ export class FlowEngine {
|
|
|
1380
1409
|
this.emitTask(t);
|
|
1381
1410
|
// process: 제자리 변환(모션 없음). transport: 이동 모션 방출.
|
|
1382
1411
|
if (t.intent === 'process')
|
|
1383
|
-
this.
|
|
1412
|
+
this.emitEquipment(eq);
|
|
1384
1413
|
else
|
|
1385
|
-
this.
|
|
1414
|
+
this.emitEquipment(eq, { fromNode: t.fromNode, toNode: t.toNode, startedAtSimMs: this.clockMs, durationMs: t.durationMs, progress: 0, elapsedMs: 0 });
|
|
1386
1415
|
}
|
|
1387
1416
|
}
|
|
1388
1417
|
/** in-progress 진행/완료 — 상태 효과는 전부 도메인(onTaskComplete). base 는 자원 해제·델타만. */
|
|
@@ -1390,7 +1419,7 @@ export class FlowEngine {
|
|
|
1390
1419
|
for (const t of this.tasks.values()) {
|
|
1391
1420
|
if (t.status !== 'in-progress')
|
|
1392
1421
|
continue;
|
|
1393
|
-
if (t.resource && this.
|
|
1422
|
+
if (t.resource && this.equipment.get(t.resource)?.status === 'down')
|
|
1394
1423
|
continue; // 설비 고장 → 작업 동결
|
|
1395
1424
|
t.remainingMs -= dt;
|
|
1396
1425
|
if (t.remainingMs > 0)
|
|
@@ -1403,20 +1432,20 @@ export class FlowEngine {
|
|
|
1403
1432
|
this.emitTask(t);
|
|
1404
1433
|
continue;
|
|
1405
1434
|
} // dwell(무자원) — 해제할 자원 없음
|
|
1406
|
-
const
|
|
1435
|
+
const eq = this.equipment.get(t.resource);
|
|
1407
1436
|
// OEE 계측: 셋업/가동 누적(가동 = 총 duration − 셋업). onTaskComplete 가 recordOutput 로 품질 보고.
|
|
1408
1437
|
const setup = t.appliedSetupMs ?? 0;
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1438
|
+
eq.setupMs += setup;
|
|
1439
|
+
eq.runMs += t.durationMs - setup;
|
|
1440
|
+
eq.status = 'idle';
|
|
1441
|
+
eq.taskId = null;
|
|
1413
1442
|
if (t.intent !== 'process')
|
|
1414
|
-
|
|
1443
|
+
eq.location = t.toNode; // 운반만 위치 이동; process 는 제자리
|
|
1415
1444
|
/* 함께 잡힌 설비도 같은 규칙으로 놓아 준다 — 대표만 풀면 나머지가 영원히 묶인다. */
|
|
1416
1445
|
for (const id of t.resources ?? []) {
|
|
1417
|
-
if (id ===
|
|
1446
|
+
if (id === eq.id)
|
|
1418
1447
|
continue;
|
|
1419
|
-
const m = this.
|
|
1448
|
+
const m = this.equipment.get(id);
|
|
1420
1449
|
if (!m)
|
|
1421
1450
|
continue;
|
|
1422
1451
|
m.setupMs += setup;
|
|
@@ -1425,10 +1454,10 @@ export class FlowEngine {
|
|
|
1425
1454
|
m.taskId = null;
|
|
1426
1455
|
if (t.intent !== 'process')
|
|
1427
1456
|
m.location = t.toNode;
|
|
1428
|
-
this.
|
|
1457
|
+
this.emitEquipment(m);
|
|
1429
1458
|
}
|
|
1430
1459
|
this.emitTask(t);
|
|
1431
|
-
this.
|
|
1460
|
+
this.emitEquipment(eq);
|
|
1432
1461
|
}
|
|
1433
1462
|
}
|
|
1434
1463
|
}
|