@operato/twin-kernel 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/contract.d.ts +17 -0
- package/dist/domain-definition.d.ts +13 -0
- package/dist/flow-engine.d.ts +11 -1
- package/dist/flow-engine.js +78 -10
- package/dist/observed-reducer.js +12 -2
- package/dist-cjs/index.cjs +81 -12
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -223,6 +223,18 @@ export interface OrderState {
|
|
|
223
223
|
status: string;
|
|
224
224
|
progress?: number;
|
|
225
225
|
held?: boolean;
|
|
226
|
+
/**
|
|
227
|
+
* 요청·이행 수량과 라인 — **진척(progress)으로 압축하지 않는다.**
|
|
228
|
+
*
|
|
229
|
+
* 예전에는 상태가 `progress` 하나만 들고 있어서, 미러에서 예측을 세우려면 **저널을 따로 읽어**
|
|
230
|
+
* 오더 원값을 되찾아야 했다(`buildForecastKernel`). 남은 데맨드(라인별 requested − fulfilled)를
|
|
231
|
+
* 모르면 "무엇을 얼마나 더 내보내야 하나" 를 재계획할 수 없기 때문이다.
|
|
232
|
+
*
|
|
233
|
+
* 진척은 이 둘에서 나오는 파생값이다 — 파생을 남기고 원본을 버린 것이 잘못이었다.
|
|
234
|
+
*/
|
|
235
|
+
requested?: number;
|
|
236
|
+
fulfilled?: number;
|
|
237
|
+
lines?: ObservedOrderLine[];
|
|
226
238
|
}
|
|
227
239
|
export type AttentionSeverity = 'low' | 'medium' | 'high' | 'critical';
|
|
228
240
|
export type AttentionState = 'active' | 'acknowledged' | 'cleared';
|
|
@@ -410,6 +422,11 @@ export interface TaskStatusDelta {
|
|
|
410
422
|
personnel?: string[];
|
|
411
423
|
/** 투입된 물리 자산(팔레트 등). */
|
|
412
424
|
assets?: string[];
|
|
425
|
+
/**
|
|
426
|
+
* 투입된 설비 전부 — `resourceRef` 는 그중 **대표**(첫 번째)다.
|
|
427
|
+
* 대표만 두면 "크레인 + 스프레더" 처럼 함께 잡히는 설비가 상태에서 사라진다.
|
|
428
|
+
*/
|
|
429
|
+
resources?: string[];
|
|
413
430
|
orderId?: string;
|
|
414
431
|
kind: string;
|
|
415
432
|
status: TaskStatus;
|
|
@@ -126,6 +126,19 @@ export interface OperationDef {
|
|
|
126
126
|
assetClass?: string;
|
|
127
127
|
quantity: number;
|
|
128
128
|
}[];
|
|
129
|
+
/**
|
|
130
|
+
* 필요 설비 — **ISA-95 `OperationsSegment.EquipmentSpecification`**(`EquipmentClassID` + `Quantity`).
|
|
131
|
+
*
|
|
132
|
+
* 예전에는 작업 하나에 설비 **한 대**만 붙었다(`resourceType` 하나). 그래서 "크레인 1대 + 스프레더
|
|
133
|
+
* 1대", "용접 로봇 2대가 함께" 같은 현장을 표현할 수 없었고, 그 동시 점유가 만드는 줄이 예측에서
|
|
134
|
+
* 사라졌다. 인원·자산과 **같은 규칙**으로 요구한다: 등급 + 대수, 부분 확보 없이 전량 아니면 대기.
|
|
135
|
+
*
|
|
136
|
+
* 미지정이면 기존 거동(`resourceType` 한 대, 없으면 아무 유휴 설비).
|
|
137
|
+
*/
|
|
138
|
+
equipmentSpecification?: {
|
|
139
|
+
equipmentClass?: string;
|
|
140
|
+
quantity: number;
|
|
141
|
+
}[];
|
|
129
142
|
}
|
|
130
143
|
/** 라우트(오퍼레이션 시퀀스) — ISA-95 ProcessSegment 연결. */
|
|
131
144
|
export interface RouteDef {
|
package/dist/flow-engine.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Attention, BoardDef, CanonicalEnvelope, Command, CommandAck, EventHandler, MoverMotion, OeeMetrics, AssetState, GeneratorSpec, PersonState, ScenarioControl, StateSnapshot, TwinKernel, Unsubscribe, NodeState, ItemState, MoverState, OrderStatusDelta, TaskState } from './contract.ts';
|
|
1
|
+
import type { Attention, BoardDef, CanonicalEnvelope, Command, CommandAck, EventHandler, MoverMotion, OeeMetrics, AssetState, GeneratorSpec, OrderState, PersonState, ScenarioControl, StateSnapshot, TwinKernel, Unsubscribe, NodeState, ItemState, MoverState, OrderStatusDelta, TaskState } from './contract.ts';
|
|
2
2
|
import type { EpcisEvent, BizTransactionElement } from './epcis.ts';
|
|
3
3
|
import type { AllocationPolicy, SlotView } from './allocation-policy.ts';
|
|
4
4
|
import type { DurationEstimator, DurationContext } from './duration-estimator.ts';
|
|
@@ -95,6 +95,8 @@ export interface FlowTask {
|
|
|
95
95
|
personnel?: string[];
|
|
96
96
|
/** 이 작업에 투입된 물리 자산(팔레트 등). */
|
|
97
97
|
assets?: string[];
|
|
98
|
+
/** 이 작업에 투입된 설비 전부 — `resource` 는 그중 대표(첫 번째). */
|
|
99
|
+
resources?: string[];
|
|
98
100
|
remainingMs: number;
|
|
99
101
|
durationMs: number;
|
|
100
102
|
orderId?: string;
|
|
@@ -255,6 +257,7 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
255
257
|
persons?: PersonState[];
|
|
256
258
|
assets?: AssetState[];
|
|
257
259
|
tasks?: TaskState[];
|
|
260
|
+
orders?: OrderState[];
|
|
258
261
|
}, orders?: OrderStatusDelta[]): void;
|
|
259
262
|
/** what-if 구성 변주 — 노드 용량 변경(fork 대상). 존재하면 true. */
|
|
260
263
|
setNodeCapacity(nodeId: string, capacity: number): boolean;
|
|
@@ -459,6 +462,7 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
459
462
|
/** 설비 상태 전이 — `taskId` 를 함께 싣는다(사람·자산 델타와 같은 규칙). 없으면 미러가 "이 설비가
|
|
460
463
|
* 무슨 일을 하는 중인가" 를 알 수 없어 작업↔자원 연결이 한쪽에서만 성립한다. */
|
|
461
464
|
protected emitMover(m: FlowMover, motion?: MoverMotion): void;
|
|
465
|
+
/** 오더 델타 — **라인까지 싣는다.** 라인이 빠지면 미러가 남은 데맨드를 라인별로 재계획할 수 없다. */
|
|
462
466
|
protected emitOrder(o: FlowOrder): void;
|
|
463
467
|
/**
|
|
464
468
|
* 자극 간격 — **계약이 선언한 네 분포를 실제로 판정한다.**
|
|
@@ -507,6 +511,12 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
507
511
|
* (반쯤 잡고 실패하면 사람이 아무 일도 못 하면서 묶인다).
|
|
508
512
|
*/
|
|
509
513
|
private claimPersonnel;
|
|
514
|
+
/**
|
|
515
|
+
* 필요 설비를 고른다 — **인원·자산과 같은 규칙**(등급으로 요구, 부분 확보 없이 전량 아니면 대기).
|
|
516
|
+
* 명세(`equipmentSpecification`)가 없으면 기존 거동: `resourceType` 한 대(없으면 아무 유휴 설비).
|
|
517
|
+
* 여기서는 고르기만 한다 — 확정은 호출부가 다른 자원까지 확보한 뒤에 한다.
|
|
518
|
+
*/
|
|
519
|
+
private claimEquipment;
|
|
510
520
|
/** 확보한 사람을 작업에 묶는다(설비까지 확정된 뒤). */
|
|
511
521
|
private assignCrew;
|
|
512
522
|
/** 작업이 끝나면 사람을 놓아 준다 — 설비 해제와 별개 경로. */
|
package/dist/flow-engine.js
CHANGED
|
@@ -288,7 +288,14 @@ export class FlowEngine {
|
|
|
288
288
|
}
|
|
289
289
|
}
|
|
290
290
|
}
|
|
291
|
-
|
|
291
|
+
/* 오더 원값은 이제 **스냅샷에 있다** — 따로 넘겨받은 것이 없으면 스냅샷에서 읽는다.
|
|
292
|
+
* (예전에는 상태가 progress 로 압축돼 호출부가 저널을 뒤져 원값을 넘겨야 했다.) */
|
|
293
|
+
const observedOrders = orders.length
|
|
294
|
+
? orders
|
|
295
|
+
: (snap.orders ?? [])
|
|
296
|
+
.filter(o => o.requested !== undefined)
|
|
297
|
+
.map(o => ({ orderId: o.id, kind: o.kind, status: o.status, requested: o.requested, fulfilled: o.fulfilled ?? 0, held: o.held, lines: o.lines }));
|
|
298
|
+
for (const o of observedOrders) {
|
|
292
299
|
const lines = (o.lines ?? []).map(l => ({ gtin: l.gtin, requested: l.requested - (l.fulfilled ?? 0) })).filter(l => l.requested > 0);
|
|
293
300
|
const remaining = lines.reduce((s, l) => s + l.requested, 0);
|
|
294
301
|
if (remaining <= 0)
|
|
@@ -491,9 +498,16 @@ export class FlowEngine {
|
|
|
491
498
|
...(t.durationMs ? { durationMs: t.durationMs } : {}),
|
|
492
499
|
...(t.status === 'in-progress' ? { remainingMs: t.remainingMs, startedAtSimMs: t.startedAtSimMs, progress: this.progressOf(t) } : {}),
|
|
493
500
|
...(t.personnel?.length ? { personnel: t.personnel.slice() } : {}),
|
|
494
|
-
...(t.assets?.length ? { assets: t.assets.slice() } : {})
|
|
501
|
+
...(t.assets?.length ? { assets: t.assets.slice() } : {}),
|
|
502
|
+
...(t.resources?.length ? { resources: t.resources.slice() } : {})
|
|
503
|
+
})),
|
|
504
|
+
orders: [...this.orders.values()].map(o => ({
|
|
505
|
+
id: o.id, kind: o.kind, status: o.status,
|
|
506
|
+
progress: o.requested ? o.fulfilled / o.requested : 0,
|
|
507
|
+
requested: o.requested, fulfilled: o.fulfilled,
|
|
508
|
+
...(o.lines?.length ? { lines: o.lines.map(l => ({ gtin: l.gtin, requested: l.requested })) } : {}),
|
|
509
|
+
held: o.held
|
|
495
510
|
})),
|
|
496
|
-
orders: [...this.orders.values()].map(o => ({ id: o.id, kind: o.kind, status: o.status, progress: o.requested ? o.fulfilled / o.requested : 0, held: o.held })),
|
|
497
511
|
attentions: this.computeAttentions()
|
|
498
512
|
};
|
|
499
513
|
}
|
|
@@ -932,6 +946,7 @@ export class FlowEngine {
|
|
|
932
946
|
intent: t.intent,
|
|
933
947
|
...(t.personnel?.length ? { personnel: t.personnel.slice() } : {}),
|
|
934
948
|
...(t.assets?.length ? { assets: t.assets.slice() } : {}),
|
|
949
|
+
...(t.resources?.length ? { resources: t.resources.slice() } : {}),
|
|
935
950
|
...(t.durationMs ? { durationMs: t.durationMs } : {}),
|
|
936
951
|
...(inProgress ? { remainingMs: t.remainingMs, startedAtSimMs: t.startedAtSimMs, progress: t.durationMs ? done / t.durationMs : undefined } : {})
|
|
937
952
|
});
|
|
@@ -948,7 +963,13 @@ export class FlowEngine {
|
|
|
948
963
|
emitMover(m, motion) {
|
|
949
964
|
this.emitOp(OP_EVENT.equipment, { moverId: m.id, kind: m.kind, status: m.status, location: m.location, taskId: m.taskId ?? undefined, motion });
|
|
950
965
|
}
|
|
951
|
-
|
|
966
|
+
/** 오더 델타 — **라인까지 싣는다.** 라인이 빠지면 미러가 남은 데맨드를 라인별로 재계획할 수 없다. */
|
|
967
|
+
emitOrder(o) {
|
|
968
|
+
this.emitOp(OP_EVENT.order, {
|
|
969
|
+
orderId: o.id, kind: o.kind, status: o.status, requested: o.requested, fulfilled: o.fulfilled, held: o.held,
|
|
970
|
+
...(o.lines?.length ? { lines: o.lines.map(l => ({ gtin: l.gtin, requested: l.requested })) } : {})
|
|
971
|
+
});
|
|
972
|
+
}
|
|
952
973
|
// ── 내부 mechanics ─────────────────────────────────────────────────────────
|
|
953
974
|
/**
|
|
954
975
|
* 자극 간격 — **계약이 선언한 네 분포를 실제로 판정한다.**
|
|
@@ -1097,6 +1118,31 @@ export class FlowEngine {
|
|
|
1097
1118
|
}
|
|
1098
1119
|
return picked;
|
|
1099
1120
|
}
|
|
1121
|
+
/**
|
|
1122
|
+
* 필요 설비를 고른다 — **인원·자산과 같은 규칙**(등급으로 요구, 부분 확보 없이 전량 아니면 대기).
|
|
1123
|
+
* 명세(`equipmentSpecification`)가 없으면 기존 거동: `resourceType` 한 대(없으면 아무 유휴 설비).
|
|
1124
|
+
* 여기서는 고르기만 한다 — 확정은 호출부가 다른 자원까지 확보한 뒤에 한다.
|
|
1125
|
+
*/
|
|
1126
|
+
claimEquipment(t) {
|
|
1127
|
+
const free = (kind, picked = []) => [...this.movers.values()].filter(m => m.status === 'idle' && !m.held && !this.offShift(m) && !picked.includes(m.id) && (kind === undefined || m.kind === kind));
|
|
1128
|
+
const need = this.operationSpecs.get(t.kind)?.equipmentSpecification;
|
|
1129
|
+
if (!need?.length) {
|
|
1130
|
+
const one = free(t.resourceType)[0];
|
|
1131
|
+
return one ? [one.id] : null;
|
|
1132
|
+
}
|
|
1133
|
+
const picked = [];
|
|
1134
|
+
for (const req of need) {
|
|
1135
|
+
const want = Math.max(0, Math.floor(req.quantity ?? 0));
|
|
1136
|
+
if (!want)
|
|
1137
|
+
continue;
|
|
1138
|
+
const avail = free(req.equipmentClass, picked);
|
|
1139
|
+
if (avail.length < want)
|
|
1140
|
+
return null; // 한 등급이라도 모자라면 시작하지 않는다
|
|
1141
|
+
for (let i = 0; i < want; i++)
|
|
1142
|
+
picked.push(avail[i].id);
|
|
1143
|
+
}
|
|
1144
|
+
return picked.length ? picked : null;
|
|
1145
|
+
}
|
|
1100
1146
|
/** 확보한 사람을 작업에 묶는다(설비까지 확정된 뒤). */
|
|
1101
1147
|
assignCrew(t, crew) {
|
|
1102
1148
|
if (!crew.length)
|
|
@@ -1305,10 +1351,12 @@ export class FlowEngine {
|
|
|
1305
1351
|
* 이 제약이 없으면 대기가 생기지 않아 병목이 사라지고 예측이 낙관 쪽으로 치우친다. */
|
|
1306
1352
|
if (this.stationFull(t.toNode))
|
|
1307
1353
|
continue;
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1354
|
+
/* 필요 설비 — 명세가 있으면 등급별 대수만큼(부분 확보 없이), 없으면 기존대로 한 대.
|
|
1355
|
+
* 확보 실패는 `null` — 다음 작업으로 넘어간다(다른 타입은 가용할 수 있다). */
|
|
1356
|
+
const rigs = this.claimEquipment(t);
|
|
1357
|
+
if (!rigs)
|
|
1358
|
+
continue;
|
|
1359
|
+
const mover = this.movers.get(rigs[0]);
|
|
1312
1360
|
// 체인지오버: task 의 changeoverKey 가 무버 직전 키와 다르면 셋업 부착(첫 작업은 셋업 없음).
|
|
1313
1361
|
if (t.setupMs && t.changeoverKey !== undefined && mover.lastChangeoverKey !== undefined && mover.lastChangeoverKey !== t.changeoverKey) {
|
|
1314
1362
|
t.appliedSetupMs = t.setupMs;
|
|
@@ -1316,12 +1364,17 @@ export class FlowEngine {
|
|
|
1316
1364
|
}
|
|
1317
1365
|
if (t.changeoverKey !== undefined)
|
|
1318
1366
|
mover.lastChangeoverKey = t.changeoverKey;
|
|
1319
|
-
|
|
1320
|
-
|
|
1367
|
+
for (const id of rigs) {
|
|
1368
|
+
const m = this.movers.get(id);
|
|
1369
|
+
m.status = 'busy';
|
|
1370
|
+
m.taskId = t.id;
|
|
1371
|
+
}
|
|
1321
1372
|
t.status = 'in-progress';
|
|
1322
1373
|
t.resource = mover.id;
|
|
1323
1374
|
t.remainingMs = t.durationMs;
|
|
1324
1375
|
t.startedAtSimMs = this.clockMs;
|
|
1376
|
+
if (rigs.length > 1)
|
|
1377
|
+
t.resources = rigs.slice(); // 대표만으로는 함께 잡힌 설비가 사라진다
|
|
1325
1378
|
this.assignCrew(t, crew);
|
|
1326
1379
|
this.assignAssets(t, gear);
|
|
1327
1380
|
this.emitTask(t);
|
|
@@ -1359,6 +1412,21 @@ export class FlowEngine {
|
|
|
1359
1412
|
mover.taskId = null;
|
|
1360
1413
|
if (t.intent !== 'process')
|
|
1361
1414
|
mover.location = t.toNode; // 운반만 위치 이동; process 는 제자리
|
|
1415
|
+
/* 함께 잡힌 설비도 같은 규칙으로 놓아 준다 — 대표만 풀면 나머지가 영원히 묶인다. */
|
|
1416
|
+
for (const id of t.resources ?? []) {
|
|
1417
|
+
if (id === mover.id)
|
|
1418
|
+
continue;
|
|
1419
|
+
const m = this.movers.get(id);
|
|
1420
|
+
if (!m)
|
|
1421
|
+
continue;
|
|
1422
|
+
m.setupMs += setup;
|
|
1423
|
+
m.runMs += t.durationMs - setup;
|
|
1424
|
+
m.status = 'idle';
|
|
1425
|
+
m.taskId = null;
|
|
1426
|
+
if (t.intent !== 'process')
|
|
1427
|
+
m.location = t.toNode;
|
|
1428
|
+
this.emitMover(m);
|
|
1429
|
+
}
|
|
1362
1430
|
this.emitTask(t);
|
|
1363
1431
|
this.emitMover(mover);
|
|
1364
1432
|
}
|
package/dist/observed-reducer.js
CHANGED
|
@@ -148,7 +148,8 @@ export class ObservedReducer {
|
|
|
148
148
|
intent: d.intent, progress: d.progress, remainingMs: d.remainingMs, durationMs: d.durationMs,
|
|
149
149
|
startedAtSimMs: d.startedAtSimMs,
|
|
150
150
|
...(d.personnel?.length ? { personnel: d.personnel.slice() } : {}),
|
|
151
|
-
...(d.assets?.length ? { assets: d.assets.slice() } : {})
|
|
151
|
+
...(d.assets?.length ? { assets: d.assets.slice() } : {}),
|
|
152
|
+
...(d.resources?.length ? { resources: d.resources.slice() } : {})
|
|
152
153
|
});
|
|
153
154
|
break;
|
|
154
155
|
}
|
|
@@ -197,7 +198,16 @@ export class ObservedReducer {
|
|
|
197
198
|
const d = e.data;
|
|
198
199
|
if (this.stale(`order:${d.orderId}`, e))
|
|
199
200
|
return;
|
|
200
|
-
|
|
201
|
+
/* **진척으로 압축하지 않는다.** 예전에는 progress 만 남겨서, 미러에서 예측을 세우려면 저널을
|
|
202
|
+
* 따로 읽어 오더 원값을 되찾아야 했다. 진척은 요청·이행에서 나오는 파생이다 — 파생을 남기고
|
|
203
|
+
* 원본을 버리면 남은 데맨드를 재계획할 수 없다. */
|
|
204
|
+
this.orders.set(d.orderId, {
|
|
205
|
+
id: d.orderId, kind: d.kind, status: d.status,
|
|
206
|
+
progress: d.requested ? d.fulfilled / d.requested : 0,
|
|
207
|
+
requested: d.requested, fulfilled: d.fulfilled,
|
|
208
|
+
...(d.lines?.length ? { lines: d.lines.map(l => ({ ...l })) } : {}),
|
|
209
|
+
held: d.held
|
|
210
|
+
});
|
|
201
211
|
break;
|
|
202
212
|
}
|
|
203
213
|
// 알 수 없는 eventType 은 무시(전방 호환).
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -639,7 +639,8 @@ var ObservedReducer = class {
|
|
|
639
639
|
durationMs: d.durationMs,
|
|
640
640
|
startedAtSimMs: d.startedAtSimMs,
|
|
641
641
|
...d.personnel?.length ? { personnel: d.personnel.slice() } : {},
|
|
642
|
-
...d.assets?.length ? { assets: d.assets.slice() } : {}
|
|
642
|
+
...d.assets?.length ? { assets: d.assets.slice() } : {},
|
|
643
|
+
...d.resources?.length ? { resources: d.resources.slice() } : {}
|
|
643
644
|
});
|
|
644
645
|
break;
|
|
645
646
|
}
|
|
@@ -681,7 +682,16 @@ var ObservedReducer = class {
|
|
|
681
682
|
case OP_EVENT.order: {
|
|
682
683
|
const d = e.data;
|
|
683
684
|
if (this.stale(`order:${d.orderId}`, e)) return;
|
|
684
|
-
this.orders.set(d.orderId, {
|
|
685
|
+
this.orders.set(d.orderId, {
|
|
686
|
+
id: d.orderId,
|
|
687
|
+
kind: d.kind,
|
|
688
|
+
status: d.status,
|
|
689
|
+
progress: d.requested ? d.fulfilled / d.requested : 0,
|
|
690
|
+
requested: d.requested,
|
|
691
|
+
fulfilled: d.fulfilled,
|
|
692
|
+
...d.lines?.length ? { lines: d.lines.map((l) => ({ ...l })) } : {},
|
|
693
|
+
held: d.held
|
|
694
|
+
});
|
|
685
695
|
break;
|
|
686
696
|
}
|
|
687
697
|
}
|
|
@@ -1448,7 +1458,8 @@ var FlowEngine = class {
|
|
|
1448
1458
|
}
|
|
1449
1459
|
}
|
|
1450
1460
|
}
|
|
1451
|
-
|
|
1461
|
+
const observedOrders = orders.length ? orders : (snap.orders ?? []).filter((o) => o.requested !== void 0).map((o) => ({ orderId: o.id, kind: o.kind, status: o.status, requested: o.requested, fulfilled: o.fulfilled ?? 0, held: o.held, lines: o.lines }));
|
|
1462
|
+
for (const o of observedOrders) {
|
|
1452
1463
|
const lines = (o.lines ?? []).map((l) => ({ gtin: l.gtin, requested: l.requested - (l.fulfilled ?? 0) })).filter((l) => l.requested > 0);
|
|
1453
1464
|
const remaining = lines.reduce((s, l) => s + l.requested, 0);
|
|
1454
1465
|
if (remaining <= 0) continue;
|
|
@@ -1644,9 +1655,19 @@ var FlowEngine = class {
|
|
|
1644
1655
|
...t.durationMs ? { durationMs: t.durationMs } : {},
|
|
1645
1656
|
...t.status === "in-progress" ? { remainingMs: t.remainingMs, startedAtSimMs: t.startedAtSimMs, progress: this.progressOf(t) } : {},
|
|
1646
1657
|
...t.personnel?.length ? { personnel: t.personnel.slice() } : {},
|
|
1647
|
-
...t.assets?.length ? { assets: t.assets.slice() } : {}
|
|
1658
|
+
...t.assets?.length ? { assets: t.assets.slice() } : {},
|
|
1659
|
+
...t.resources?.length ? { resources: t.resources.slice() } : {}
|
|
1660
|
+
})),
|
|
1661
|
+
orders: [...this.orders.values()].map((o) => ({
|
|
1662
|
+
id: o.id,
|
|
1663
|
+
kind: o.kind,
|
|
1664
|
+
status: o.status,
|
|
1665
|
+
progress: o.requested ? o.fulfilled / o.requested : 0,
|
|
1666
|
+
requested: o.requested,
|
|
1667
|
+
fulfilled: o.fulfilled,
|
|
1668
|
+
...o.lines?.length ? { lines: o.lines.map((l) => ({ gtin: l.gtin, requested: l.requested })) } : {},
|
|
1669
|
+
held: o.held
|
|
1648
1670
|
})),
|
|
1649
|
-
orders: [...this.orders.values()].map((o) => ({ id: o.id, kind: o.kind, status: o.status, progress: o.requested ? o.fulfilled / o.requested : 0, held: o.held })),
|
|
1650
1671
|
attentions: this.computeAttentions()
|
|
1651
1672
|
};
|
|
1652
1673
|
}
|
|
@@ -2040,6 +2061,7 @@ var FlowEngine = class {
|
|
|
2040
2061
|
intent: t.intent,
|
|
2041
2062
|
...t.personnel?.length ? { personnel: t.personnel.slice() } : {},
|
|
2042
2063
|
...t.assets?.length ? { assets: t.assets.slice() } : {},
|
|
2064
|
+
...t.resources?.length ? { resources: t.resources.slice() } : {},
|
|
2043
2065
|
...t.durationMs ? { durationMs: t.durationMs } : {},
|
|
2044
2066
|
...inProgress ? { remainingMs: t.remainingMs, startedAtSimMs: t.startedAtSimMs, progress: t.durationMs ? done / t.durationMs : void 0 } : {}
|
|
2045
2067
|
});
|
|
@@ -2056,8 +2078,17 @@ var FlowEngine = class {
|
|
|
2056
2078
|
emitMover(m, motion) {
|
|
2057
2079
|
this.emitOp(OP_EVENT.equipment, { moverId: m.id, kind: m.kind, status: m.status, location: m.location, taskId: m.taskId ?? void 0, motion });
|
|
2058
2080
|
}
|
|
2081
|
+
/** 오더 델타 — **라인까지 싣는다.** 라인이 빠지면 미러가 남은 데맨드를 라인별로 재계획할 수 없다. */
|
|
2059
2082
|
emitOrder(o) {
|
|
2060
|
-
this.emitOp(OP_EVENT.order, {
|
|
2083
|
+
this.emitOp(OP_EVENT.order, {
|
|
2084
|
+
orderId: o.id,
|
|
2085
|
+
kind: o.kind,
|
|
2086
|
+
status: o.status,
|
|
2087
|
+
requested: o.requested,
|
|
2088
|
+
fulfilled: o.fulfilled,
|
|
2089
|
+
held: o.held,
|
|
2090
|
+
...o.lines?.length ? { lines: o.lines.map((l) => ({ gtin: l.gtin, requested: l.requested })) } : {}
|
|
2091
|
+
});
|
|
2061
2092
|
}
|
|
2062
2093
|
// ── 내부 mechanics ─────────────────────────────────────────────────────────
|
|
2063
2094
|
/**
|
|
@@ -2189,6 +2220,30 @@ var FlowEngine = class {
|
|
|
2189
2220
|
}
|
|
2190
2221
|
return picked;
|
|
2191
2222
|
}
|
|
2223
|
+
/**
|
|
2224
|
+
* 필요 설비를 고른다 — **인원·자산과 같은 규칙**(등급으로 요구, 부분 확보 없이 전량 아니면 대기).
|
|
2225
|
+
* 명세(`equipmentSpecification`)가 없으면 기존 거동: `resourceType` 한 대(없으면 아무 유휴 설비).
|
|
2226
|
+
* 여기서는 고르기만 한다 — 확정은 호출부가 다른 자원까지 확보한 뒤에 한다.
|
|
2227
|
+
*/
|
|
2228
|
+
claimEquipment(t) {
|
|
2229
|
+
const free = (kind, picked2 = []) => [...this.movers.values()].filter(
|
|
2230
|
+
(m) => m.status === "idle" && !m.held && !this.offShift(m) && !picked2.includes(m.id) && (kind === void 0 || m.kind === kind)
|
|
2231
|
+
);
|
|
2232
|
+
const need = this.operationSpecs.get(t.kind)?.equipmentSpecification;
|
|
2233
|
+
if (!need?.length) {
|
|
2234
|
+
const one = free(t.resourceType)[0];
|
|
2235
|
+
return one ? [one.id] : null;
|
|
2236
|
+
}
|
|
2237
|
+
const picked = [];
|
|
2238
|
+
for (const req of need) {
|
|
2239
|
+
const want = Math.max(0, Math.floor(req.quantity ?? 0));
|
|
2240
|
+
if (!want) continue;
|
|
2241
|
+
const avail = free(req.equipmentClass, picked);
|
|
2242
|
+
if (avail.length < want) return null;
|
|
2243
|
+
for (let i = 0; i < want; i++) picked.push(avail[i].id);
|
|
2244
|
+
}
|
|
2245
|
+
return picked.length ? picked : null;
|
|
2246
|
+
}
|
|
2192
2247
|
/** 확보한 사람을 작업에 묶는다(설비까지 확정된 뒤). */
|
|
2193
2248
|
assignCrew(t, crew) {
|
|
2194
2249
|
if (!crew.length) return;
|
|
@@ -2365,21 +2420,24 @@ var FlowEngine = class {
|
|
|
2365
2420
|
continue;
|
|
2366
2421
|
}
|
|
2367
2422
|
if (this.stationFull(t.toNode)) continue;
|
|
2368
|
-
const
|
|
2369
|
-
|
|
2370
|
-
);
|
|
2371
|
-
if (!mover) continue;
|
|
2423
|
+
const rigs = this.claimEquipment(t);
|
|
2424
|
+
if (!rigs) continue;
|
|
2425
|
+
const mover = this.movers.get(rigs[0]);
|
|
2372
2426
|
if (t.setupMs && t.changeoverKey !== void 0 && mover.lastChangeoverKey !== void 0 && mover.lastChangeoverKey !== t.changeoverKey) {
|
|
2373
2427
|
t.appliedSetupMs = t.setupMs;
|
|
2374
2428
|
t.durationMs += t.setupMs;
|
|
2375
2429
|
}
|
|
2376
2430
|
if (t.changeoverKey !== void 0) mover.lastChangeoverKey = t.changeoverKey;
|
|
2377
|
-
|
|
2378
|
-
|
|
2431
|
+
for (const id of rigs) {
|
|
2432
|
+
const m = this.movers.get(id);
|
|
2433
|
+
m.status = "busy";
|
|
2434
|
+
m.taskId = t.id;
|
|
2435
|
+
}
|
|
2379
2436
|
t.status = "in-progress";
|
|
2380
2437
|
t.resource = mover.id;
|
|
2381
2438
|
t.remainingMs = t.durationMs;
|
|
2382
2439
|
t.startedAtSimMs = this.clockMs;
|
|
2440
|
+
if (rigs.length > 1) t.resources = rigs.slice();
|
|
2383
2441
|
this.assignCrew(t, crew);
|
|
2384
2442
|
this.assignAssets(t, gear);
|
|
2385
2443
|
this.emitTask(t);
|
|
@@ -2409,6 +2467,17 @@ var FlowEngine = class {
|
|
|
2409
2467
|
mover.status = "idle";
|
|
2410
2468
|
mover.taskId = null;
|
|
2411
2469
|
if (t.intent !== "process") mover.location = t.toNode;
|
|
2470
|
+
for (const id of t.resources ?? []) {
|
|
2471
|
+
if (id === mover.id) continue;
|
|
2472
|
+
const m = this.movers.get(id);
|
|
2473
|
+
if (!m) continue;
|
|
2474
|
+
m.setupMs += setup;
|
|
2475
|
+
m.runMs += t.durationMs - setup;
|
|
2476
|
+
m.status = "idle";
|
|
2477
|
+
m.taskId = null;
|
|
2478
|
+
if (t.intent !== "process") m.location = t.toNode;
|
|
2479
|
+
this.emitMover(m);
|
|
2480
|
+
}
|
|
2412
2481
|
this.emitTask(t);
|
|
2413
2482
|
this.emitMover(mover);
|
|
2414
2483
|
}
|
package/package.json
CHANGED