@operato/twin-kernel 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/contract.d.ts +20 -0
- package/dist/event-journal.d.ts +9 -9
- package/dist/flow-engine.d.ts +31 -1
- package/dist/flow-engine.js +104 -11
- package/dist/observed-reducer.d.ts +2 -8
- package/dist-cjs/index.cjs +88 -10
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -1421,8 +1421,28 @@ export interface ProductionSpec {
|
|
|
1421
1421
|
/** 쓸 레시피 키(미지정 시 첫 레시피) — 직렬 생산 경로(MES)만 쓴다. */
|
|
1422
1422
|
recipeKey?: string;
|
|
1423
1423
|
}
|
|
1424
|
+
/**
|
|
1425
|
+
* 공장을 갈아탄 결과 — **몇 개가 늘고 몇 개가 사라졌나.**
|
|
1426
|
+
*
|
|
1427
|
+
* 돌려주지 않으면 조용히 사라진다. 자리 하나가 없어진 것을 사용자가 화면의 수를 세어 눈치채기를
|
|
1428
|
+
* 기대할 수는 없다(특히 자리가 수천인 현장에서). 그래서 갈아타기는 **말없이 성공하지 않는다.**
|
|
1429
|
+
*/
|
|
1430
|
+
export interface StructureShift {
|
|
1431
|
+
locationsAdded: number;
|
|
1432
|
+
locationsDropped: number;
|
|
1433
|
+
equipmentDropped: number;
|
|
1434
|
+
personsDropped: number;
|
|
1435
|
+
assetsDropped: number;
|
|
1436
|
+
}
|
|
1424
1437
|
export interface TwinKernel {
|
|
1425
1438
|
loadTwinModel(def: TwinModelDef): void;
|
|
1439
|
+
/**
|
|
1440
|
+
* 돌면서 공장을 갈아탄다 — 관측 구동(미러)에서만. 시뮬레이션은 거절한다(멈추고 다시 세운다).
|
|
1441
|
+
* 관측으로 알게 된 자리는 새 선언에 없어도 남고, 선언에서 사라진 자원은 버리되 수를 돌려준다.
|
|
1442
|
+
*/
|
|
1443
|
+
adoptStructure(def: TwinModelDef): StructureShift;
|
|
1444
|
+
/** 이 커널이 관측으로 구동됨을 선언한다 — 첫 이벤트가 오기 전에도 그렇다. */
|
|
1445
|
+
observe(): void;
|
|
1426
1446
|
getSnapshot(): StateSnapshot;
|
|
1427
1447
|
onEvent(handler: EventHandler): Unsubscribe;
|
|
1428
1448
|
dispatch(cmd: Command): CommandAck;
|
package/dist/event-journal.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TwinModelDef, CanonicalEnvelope } from './contract.ts';
|
|
1
|
+
import type { TwinModelDef, CanonicalEnvelope, StructureShift } from './contract.ts';
|
|
2
2
|
import { type ProjectedState } from './state-projector.ts';
|
|
3
3
|
export declare class EventJournal {
|
|
4
4
|
private events;
|
|
@@ -20,14 +20,14 @@ export interface StructureSegment {
|
|
|
20
20
|
board: TwinModelDef;
|
|
21
21
|
events: readonly CanonicalEnvelope[];
|
|
22
22
|
}
|
|
23
|
-
/**
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* 구조가 바뀐 지점마다 무엇이 사라졌는지. 조용히 넘어가지 않는다.
|
|
25
|
+
*
|
|
26
|
+
* 갈아타기의 결과 자체는 `StructureShift`(계약) 한 벌이고, 재생은 거기에 **몇 번째 마디였나**만
|
|
27
|
+
* 더한다 — 두 벌로 두면 한쪽에 필드가 늘어날 때 다른 쪽이 조용히 뒤처진다.
|
|
28
|
+
*/
|
|
29
|
+
export interface SegmentShift extends StructureShift {
|
|
25
30
|
index: number;
|
|
26
|
-
locationsAdded: number;
|
|
27
|
-
locationsDropped: number;
|
|
28
|
-
equipmentDropped: number;
|
|
29
|
-
personsDropped: number;
|
|
30
|
-
assetsDropped: number;
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
33
|
* **구조가 바뀐 이력까지 이어서 재생한다.**
|
|
@@ -43,5 +43,5 @@ export interface StructureShift {
|
|
|
43
43
|
*/
|
|
44
44
|
export declare function replaySegments(segments: readonly StructureSegment[]): {
|
|
45
45
|
state: ProjectedState;
|
|
46
|
-
shifts:
|
|
46
|
+
shifts: SegmentShift[];
|
|
47
47
|
};
|
package/dist/flow-engine.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ISOTime, MaterialQuantity, WorkCalendarEntry, EffectivePeriod, Effectivity, OffCalendarReason, ResourceProperty, ResourceClassDef, MaterialDefinition, Attention, TwinModelDef, CanonicalEnvelope, Command, CommandAck, EventHandler, EquipmentMotion, OeeMetrics, AssetState, GeneratorSpec, OrderState, PersonState, ScenarioControl, StateSnapshot, TwinKernel, Unsubscribe, LocationState, ItemState, EquipmentState, OrderStatusDelta, TaskState } from './contract.ts';
|
|
1
|
+
import type { ISOTime, MaterialQuantity, WorkCalendarEntry, EffectivePeriod, Effectivity, OffCalendarReason, ResourceProperty, ResourceClassDef, MaterialDefinition, Attention, TwinModelDef, CanonicalEnvelope, Command, CommandAck, EventHandler, EquipmentMotion, OeeMetrics, AssetState, GeneratorSpec, OrderState, PersonState, ScenarioControl, StateSnapshot, TwinKernel, Unsubscribe, LocationState, ItemState, EquipmentState, OrderStatusDelta, TaskState, StructureShift } 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';
|
|
@@ -311,6 +311,36 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
311
311
|
*/
|
|
312
312
|
protected abstract onTaskComplete(task: FlowTask): void;
|
|
313
313
|
loadTwinModel(def: TwinModelDef): void;
|
|
314
|
+
private buildLocation;
|
|
315
|
+
private buildPerson;
|
|
316
|
+
private buildAsset;
|
|
317
|
+
/**
|
|
318
|
+
* 이 커널이 **관측으로 구동된다**고 선언한다 — 미러가 첫 이벤트를 받기 전에도 그렇다.
|
|
319
|
+
*
|
|
320
|
+
* 지금까지는 첫 `apply()` 가 이 사실을 뒤늦게 정했다. 그래서 "이 커널의 진실이 어디서 오나" 가
|
|
321
|
+
* **먼저 도착한 호출이 무엇이냐**에 달려 있었다 — 아직 아무 이벤트도 안 온 미러는 스스로를
|
|
322
|
+
* 시뮬레이션으로 여긴다. 세우는 쪽이 아는 사실이므로 세울 때 말하게 한다.
|
|
323
|
+
*/
|
|
324
|
+
observe(): void;
|
|
325
|
+
/**
|
|
326
|
+
* 돌면서 공장을 갈아탄다 — **현실이 안 멈추므로 미러도 멈출 수 없다.**
|
|
327
|
+
*
|
|
328
|
+
* ── 왜 관측 구동에만 여는가 ────────────────────────────────────────────────
|
|
329
|
+
* 미러는 이미 있는 현실을 따라갈 뿐이라, 설비가 한 대 늘었다고 멈췄다 서는 것은 그 사이의
|
|
330
|
+
* 사실을 잃는 것이다(라이브에서 유실은 곧 거짓이다). 반면 **시뮬레이션은 멈춰도 된다** —
|
|
331
|
+
* 조건이 바뀐 실험은 다른 실험이고, 진행 중인 작업이 사라진 자리를 가리키게 두느니 새로
|
|
332
|
+
* 세우는 것이 옳다. 그래서 여기서 거절하는 것은 한계가 아니라 **규칙**이다.
|
|
333
|
+
*
|
|
334
|
+
* ── 무엇을 지키고 무엇을 버리는가 ──────────────────────────────────────────
|
|
335
|
+
* 판단은 관측층(`ObservedReducer.adoptStructure`)이 한다 — 관측은 지키고, 선언에서 사라진
|
|
336
|
+
* 자원은 버리되 **몇 개를 버렸는지 돌려준다.** 여기서는 그 판단을 커널 자신의 선언 표에도
|
|
337
|
+
* 그대로 옮긴다. 관측층만 갈면 사라진 설비가 커널 표에 남아 **화면이 없는 설비를 그린다.**
|
|
338
|
+
*
|
|
339
|
+
* 관측으로 알게 된 자리는 선언에 없어도 지운 뒤 다시 심는다 — 다음 스냅샷에서 관측층이
|
|
340
|
+
* 되살리므로(`settleObserved` → `hydrateObserved`), 여기서 지켜야 할 것은 **선언분**뿐이다.
|
|
341
|
+
*/
|
|
342
|
+
adoptStructure(def: TwinModelDef): StructureShift;
|
|
343
|
+
private buildEquipment;
|
|
314
344
|
/**
|
|
315
345
|
* what-if 구성 변주 — fork(또는 실행 중) 엔진에 설비 추가. loadTwinModel 설비 삽입과 동일 규약.
|
|
316
346
|
* 기본은 mtbf 미지정(고장 없는 신뢰 자원) → sampleExp(rng) 무소비라 baseline fork 와 깨끗이 비교 가능.
|
package/dist/flow-engine.js
CHANGED
|
@@ -265,22 +265,115 @@ export class FlowEngine {
|
|
|
265
265
|
if (def.productionSpec?.definition?.operations?.length)
|
|
266
266
|
this.loadOperations(def.productionSpec.definition.operations);
|
|
267
267
|
for (const n of readBoardLocations(def))
|
|
268
|
-
this.locations.set(n.id,
|
|
268
|
+
this.locations.set(n.id, this.buildLocation(n));
|
|
269
269
|
this.classDefs = { personnel: def.personnelClasses, equipment: def.equipmentClasses, asset: def.assetClasses, material: def.materialClasses };
|
|
270
270
|
this.materialDefs = new Map((def.materialDefinitions ?? []).filter(d => d?.id).map(d => [d.id, d]));
|
|
271
271
|
for (const p of def.persons ?? [])
|
|
272
|
-
this.persons.set(p.id,
|
|
272
|
+
this.persons.set(p.id, this.buildPerson(p));
|
|
273
273
|
for (const a of readBoardAssets(def))
|
|
274
|
-
this.assets.set(a.id,
|
|
275
|
-
for (const m of readBoardEquipment(def))
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
274
|
+
this.assets.set(a.id, this.buildAsset(a));
|
|
275
|
+
for (const m of readBoardEquipment(def))
|
|
276
|
+
this.equipment.set(m.id, this.buildEquipment(m));
|
|
277
|
+
}
|
|
278
|
+
/*
|
|
279
|
+
* ── 선언 하나를 상태 하나로 짓는 자리 ─────────────────────────────────────
|
|
280
|
+
* 최초 적재(`loadTwinModel`)와 가동 중 교체(`adoptStructure`)가 **같은 구성을 쓴다.** 두 곳에서
|
|
281
|
+
* 따로 지으면 곧 갈라지고, 갈라진 쪽으로 들어온 자원만 필드 하나가 비는 식으로 조용히 다르다.
|
|
282
|
+
*/
|
|
283
|
+
buildLocation(n) {
|
|
284
|
+
return { id: n.id, type: n.type, capacity: n.capacity, parallelism: n.parallelism, occupancy: 0, status: 'idle', parentId: n.parentId };
|
|
285
|
+
}
|
|
286
|
+
buildPerson(p) {
|
|
287
|
+
return { id: p.id, personnelClassIds: p.personnelClassIds, status: 'idle', taskId: null, window: p.window, ...(p.workCalendar ? { workCalendar: p.workCalendar } : {}), ...(p.homeLocation ? { location: p.homeLocation } : {}), ...(p.properties ? { properties: p.properties } : {}), ...(p.testSpecificationIds ? { testSpecificationIds: p.testSpecificationIds } : {}), ...effectiveOnly(p) };
|
|
288
|
+
}
|
|
289
|
+
buildAsset(a) {
|
|
290
|
+
return { id: a.id, assetClassIds: a.assetClassIds, location: a.homeLocation, status: 'idle', taskId: null, ...(a.properties ? { properties: a.properties } : {}), ...(a.testSpecificationIds ? { testSpecificationIds: a.testSpecificationIds } : {}), ...effectiveOnly(a) };
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* 이 커널이 **관측으로 구동된다**고 선언한다 — 미러가 첫 이벤트를 받기 전에도 그렇다.
|
|
294
|
+
*
|
|
295
|
+
* 지금까지는 첫 `apply()` 가 이 사실을 뒤늦게 정했다. 그래서 "이 커널의 진실이 어디서 오나" 가
|
|
296
|
+
* **먼저 도착한 호출이 무엇이냐**에 달려 있었다 — 아직 아무 이벤트도 안 온 미러는 스스로를
|
|
297
|
+
* 시뮬레이션으로 여긴다. 세우는 쪽이 아는 사실이므로 세울 때 말하게 한다.
|
|
298
|
+
*/
|
|
299
|
+
observe() {
|
|
300
|
+
this.observeMode = true;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* 돌면서 공장을 갈아탄다 — **현실이 안 멈추므로 미러도 멈출 수 없다.**
|
|
304
|
+
*
|
|
305
|
+
* ── 왜 관측 구동에만 여는가 ────────────────────────────────────────────────
|
|
306
|
+
* 미러는 이미 있는 현실을 따라갈 뿐이라, 설비가 한 대 늘었다고 멈췄다 서는 것은 그 사이의
|
|
307
|
+
* 사실을 잃는 것이다(라이브에서 유실은 곧 거짓이다). 반면 **시뮬레이션은 멈춰도 된다** —
|
|
308
|
+
* 조건이 바뀐 실험은 다른 실험이고, 진행 중인 작업이 사라진 자리를 가리키게 두느니 새로
|
|
309
|
+
* 세우는 것이 옳다. 그래서 여기서 거절하는 것은 한계가 아니라 **규칙**이다.
|
|
310
|
+
*
|
|
311
|
+
* ── 무엇을 지키고 무엇을 버리는가 ──────────────────────────────────────────
|
|
312
|
+
* 판단은 관측층(`ObservedReducer.adoptStructure`)이 한다 — 관측은 지키고, 선언에서 사라진
|
|
313
|
+
* 자원은 버리되 **몇 개를 버렸는지 돌려준다.** 여기서는 그 판단을 커널 자신의 선언 표에도
|
|
314
|
+
* 그대로 옮긴다. 관측층만 갈면 사라진 설비가 커널 표에 남아 **화면이 없는 설비를 그린다.**
|
|
315
|
+
*
|
|
316
|
+
* 관측으로 알게 된 자리는 선언에 없어도 지운 뒤 다시 심는다 — 다음 스냅샷에서 관측층이
|
|
317
|
+
* 되살리므로(`settleObserved` → `hydrateObserved`), 여기서 지켜야 할 것은 **선언분**뿐이다.
|
|
318
|
+
*/
|
|
319
|
+
adoptStructure(def) {
|
|
320
|
+
if (!this.observeMode)
|
|
321
|
+
throw new Error('adoptStructure is for observation-driven kernels (mirrors) — a simulation cannot swap its factory mid-run; stop and re-provision instead');
|
|
322
|
+
this.boardDef = def;
|
|
323
|
+
if (def.productionSpec?.definition?.operations?.length)
|
|
324
|
+
this.loadOperations(def.productionSpec.definition.operations);
|
|
325
|
+
this.classDefs = { personnel: def.personnelClasses, equipment: def.equipmentClasses, asset: def.assetClasses, material: def.materialClasses };
|
|
326
|
+
this.materialDefs = new Map((def.materialDefinitions ?? []).filter(d => d?.id).map(d => [d.id, d]));
|
|
327
|
+
/* **살아남은 것은 그대로 둔다** — 다시 지으면 가동시간·양품수가 0 으로 돌아간다(OEE 가 리셋된다).
|
|
328
|
+
선언이 바뀐 트윈에서 성과 지표가 조용히 끊기는 것이 이 갈아타기의 가장 비싼 오류다. */
|
|
329
|
+
const sync = (map, declared, build) => {
|
|
330
|
+
const ids = new Set(declared.map(d => d.id));
|
|
331
|
+
let dropped = 0;
|
|
332
|
+
for (const id of [...map.keys()])
|
|
333
|
+
if (!ids.has(id)) {
|
|
334
|
+
map.delete(id);
|
|
335
|
+
dropped++;
|
|
336
|
+
}
|
|
337
|
+
for (const d of declared)
|
|
338
|
+
if (!map.has(d.id))
|
|
339
|
+
map.set(d.id, build(d));
|
|
340
|
+
return dropped;
|
|
341
|
+
};
|
|
342
|
+
const declaredLocations = readBoardLocations(def);
|
|
343
|
+
const locationsBefore = this.locations.size;
|
|
344
|
+
const locationsDropped = sync(this.locations, declaredLocations, n => this.buildLocation(n));
|
|
345
|
+
const equipmentDropped = sync(this.equipment, readBoardEquipment(def), m => this.buildEquipment(m));
|
|
346
|
+
const personsDropped = sync(this.persons, def.persons ?? [], p => this.buildPerson(p));
|
|
347
|
+
const assetsDropped = sync(this.assets, readBoardAssets(def), a => this.buildAsset(a));
|
|
348
|
+
const observed = this.observer?.adoptStructure(def);
|
|
349
|
+
if (this.observer)
|
|
350
|
+
this.observedDirty = true;
|
|
351
|
+
this.revision++;
|
|
352
|
+
/*
|
|
353
|
+
* **돌려주는 수는 실제로 일어난 일이어야 한다.**
|
|
354
|
+
*
|
|
355
|
+
* 자리는 관측층의 셈을 쓴다 — 관측으로 알게 된 자리는 선언에 없어도 남으므로, 커널 표만 보면
|
|
356
|
+
* "버렸다" 고 세었다가 다음 스냅샷에서 되살아난다(그 수를 그대로 보고하면 거짓말이 된다).
|
|
357
|
+
* 관측층이 아직 없으면(첫 이벤트 전) 커널 표의 셈이 곧 사실이다.
|
|
358
|
+
*
|
|
359
|
+
* 자원은 어느 층에서나 **선언이 전부**라 셈이 같고, 화면이 그리는 것은 커널 표이므로 이쪽을 쓴다.
|
|
360
|
+
*/
|
|
361
|
+
return {
|
|
362
|
+
locationsAdded: observed?.locationsAdded ?? Math.max(0, this.locations.size - locationsBefore + locationsDropped),
|
|
363
|
+
locationsDropped: observed?.locationsDropped ?? locationsDropped,
|
|
364
|
+
equipmentDropped,
|
|
365
|
+
personsDropped,
|
|
366
|
+
assetsDropped
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
buildEquipment(m) {
|
|
370
|
+
const eq = { id: m.id, kind: m.kind, location: m.homeLocation, homeLocation: m.homeLocation, ...(m.properties ? { properties: m.properties } : {}), ...(m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}), status: 'idle', taskId: null, runMs: 0, setupMs: 0, downMs: 0, goodCount: 0, scrapCount: 0, window: m.window, ...(m.workCalendar ? { workCalendar: m.workCalendar } : {}), ...effectiveOnly(m) };
|
|
371
|
+
if (m.mtbfMs !== undefined) {
|
|
372
|
+
eq.mtbfMs = m.mtbfMs;
|
|
373
|
+
eq.mttrMs = m.mttrMs;
|
|
374
|
+
eq.nextFailureMs = this.sampleExp(m.mtbfMs);
|
|
283
375
|
}
|
|
376
|
+
return eq;
|
|
284
377
|
}
|
|
285
378
|
/**
|
|
286
379
|
* what-if 구성 변주 — fork(또는 실행 중) 엔진에 설비 추가. loadTwinModel 설비 삽입과 동일 규약.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AssetState, TwinModelDef, CanonicalEnvelope, LocationState, ItemState, EquipmentState, PersonState, TaskState, OrderState } from './contract.ts';
|
|
1
|
+
import type { AssetState, TwinModelDef, CanonicalEnvelope, LocationState, ItemState, EquipmentState, PersonState, TaskState, OrderState, StructureShift } from './contract.ts';
|
|
2
2
|
/**
|
|
3
3
|
* 마스터 동기 — 선언적 로케이션 upsert/remove.
|
|
4
4
|
*
|
|
@@ -91,13 +91,7 @@ export declare class ObservedReducer {
|
|
|
91
91
|
* 관측으로 알게 된 자리(`origin: 'observed'`)는 **새 마스터에 없어도 남긴다** — 마스터가 모르는
|
|
92
92
|
* 자리에서 물건이 실제로 보였다는 사실은 구조를 바꾼다고 사라지지 않는다.
|
|
93
93
|
*/
|
|
94
|
-
adoptStructure(board: TwinModelDef):
|
|
95
|
-
locationsAdded: number;
|
|
96
|
-
locationsDropped: number;
|
|
97
|
-
equipmentDropped: number;
|
|
98
|
-
personsDropped: number;
|
|
99
|
-
assetsDropped: number;
|
|
100
|
-
};
|
|
94
|
+
adoptStructure(board: TwinModelDef): StructureShift;
|
|
101
95
|
/** 마스터 동기 — 로케이션 추가/변경/제거. */
|
|
102
96
|
applyMaster(u: MasterUpdate): void;
|
|
103
97
|
/**
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -2283,20 +2283,98 @@ var FlowEngine = class {
|
|
|
2283
2283
|
loadTwinModel(def) {
|
|
2284
2284
|
this.boardDef = def;
|
|
2285
2285
|
if (def.productionSpec?.definition?.operations?.length) this.loadOperations(def.productionSpec.definition.operations);
|
|
2286
|
-
for (const n of readBoardLocations(def)) this.locations.set(n.id,
|
|
2286
|
+
for (const n of readBoardLocations(def)) this.locations.set(n.id, this.buildLocation(n));
|
|
2287
2287
|
this.classDefs = { personnel: def.personnelClasses, equipment: def.equipmentClasses, asset: def.assetClasses, material: def.materialClasses };
|
|
2288
2288
|
this.materialDefs = new Map((def.materialDefinitions ?? []).filter((d) => d?.id).map((d) => [d.id, d]));
|
|
2289
|
-
for (const p of def.persons ?? []) this.persons.set(p.id,
|
|
2290
|
-
for (const a of readBoardAssets(def)) this.assets.set(a.id,
|
|
2291
|
-
for (const m of readBoardEquipment(def))
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2289
|
+
for (const p of def.persons ?? []) this.persons.set(p.id, this.buildPerson(p));
|
|
2290
|
+
for (const a of readBoardAssets(def)) this.assets.set(a.id, this.buildAsset(a));
|
|
2291
|
+
for (const m of readBoardEquipment(def)) this.equipment.set(m.id, this.buildEquipment(m));
|
|
2292
|
+
}
|
|
2293
|
+
/*
|
|
2294
|
+
* ── 선언 하나를 상태 하나로 짓는 자리 ─────────────────────────────────────
|
|
2295
|
+
* 최초 적재(`loadTwinModel`)와 가동 중 교체(`adoptStructure`)가 **같은 구성을 쓴다.** 두 곳에서
|
|
2296
|
+
* 따로 지으면 곧 갈라지고, 갈라진 쪽으로 들어온 자원만 필드 하나가 비는 식으로 조용히 다르다.
|
|
2297
|
+
*/
|
|
2298
|
+
buildLocation(n) {
|
|
2299
|
+
return { id: n.id, type: n.type, capacity: n.capacity, parallelism: n.parallelism, occupancy: 0, status: "idle", parentId: n.parentId };
|
|
2300
|
+
}
|
|
2301
|
+
buildPerson(p) {
|
|
2302
|
+
return { id: p.id, personnelClassIds: p.personnelClassIds, status: "idle", taskId: null, window: p.window, ...p.workCalendar ? { workCalendar: p.workCalendar } : {}, ...p.homeLocation ? { location: p.homeLocation } : {}, ...p.properties ? { properties: p.properties } : {}, ...p.testSpecificationIds ? { testSpecificationIds: p.testSpecificationIds } : {}, ...effectiveOnly(p) };
|
|
2303
|
+
}
|
|
2304
|
+
buildAsset(a) {
|
|
2305
|
+
return { id: a.id, assetClassIds: a.assetClassIds, location: a.homeLocation, status: "idle", taskId: null, ...a.properties ? { properties: a.properties } : {}, ...a.testSpecificationIds ? { testSpecificationIds: a.testSpecificationIds } : {}, ...effectiveOnly(a) };
|
|
2306
|
+
}
|
|
2307
|
+
/**
|
|
2308
|
+
* 이 커널이 **관측으로 구동된다**고 선언한다 — 미러가 첫 이벤트를 받기 전에도 그렇다.
|
|
2309
|
+
*
|
|
2310
|
+
* 지금까지는 첫 `apply()` 가 이 사실을 뒤늦게 정했다. 그래서 "이 커널의 진실이 어디서 오나" 가
|
|
2311
|
+
* **먼저 도착한 호출이 무엇이냐**에 달려 있었다 — 아직 아무 이벤트도 안 온 미러는 스스로를
|
|
2312
|
+
* 시뮬레이션으로 여긴다. 세우는 쪽이 아는 사실이므로 세울 때 말하게 한다.
|
|
2313
|
+
*/
|
|
2314
|
+
observe() {
|
|
2315
|
+
this.observeMode = true;
|
|
2316
|
+
}
|
|
2317
|
+
/**
|
|
2318
|
+
* 돌면서 공장을 갈아탄다 — **현실이 안 멈추므로 미러도 멈출 수 없다.**
|
|
2319
|
+
*
|
|
2320
|
+
* ── 왜 관측 구동에만 여는가 ────────────────────────────────────────────────
|
|
2321
|
+
* 미러는 이미 있는 현실을 따라갈 뿐이라, 설비가 한 대 늘었다고 멈췄다 서는 것은 그 사이의
|
|
2322
|
+
* 사실을 잃는 것이다(라이브에서 유실은 곧 거짓이다). 반면 **시뮬레이션은 멈춰도 된다** —
|
|
2323
|
+
* 조건이 바뀐 실험은 다른 실험이고, 진행 중인 작업이 사라진 자리를 가리키게 두느니 새로
|
|
2324
|
+
* 세우는 것이 옳다. 그래서 여기서 거절하는 것은 한계가 아니라 **규칙**이다.
|
|
2325
|
+
*
|
|
2326
|
+
* ── 무엇을 지키고 무엇을 버리는가 ──────────────────────────────────────────
|
|
2327
|
+
* 판단은 관측층(`ObservedReducer.adoptStructure`)이 한다 — 관측은 지키고, 선언에서 사라진
|
|
2328
|
+
* 자원은 버리되 **몇 개를 버렸는지 돌려준다.** 여기서는 그 판단을 커널 자신의 선언 표에도
|
|
2329
|
+
* 그대로 옮긴다. 관측층만 갈면 사라진 설비가 커널 표에 남아 **화면이 없는 설비를 그린다.**
|
|
2330
|
+
*
|
|
2331
|
+
* 관측으로 알게 된 자리는 선언에 없어도 지운 뒤 다시 심는다 — 다음 스냅샷에서 관측층이
|
|
2332
|
+
* 되살리므로(`settleObserved` → `hydrateObserved`), 여기서 지켜야 할 것은 **선언분**뿐이다.
|
|
2333
|
+
*/
|
|
2334
|
+
adoptStructure(def) {
|
|
2335
|
+
if (!this.observeMode)
|
|
2336
|
+
throw new Error(
|
|
2337
|
+
"adoptStructure is for observation-driven kernels (mirrors) \u2014 a simulation cannot swap its factory mid-run; stop and re-provision instead"
|
|
2338
|
+
);
|
|
2339
|
+
this.boardDef = def;
|
|
2340
|
+
if (def.productionSpec?.definition?.operations?.length) this.loadOperations(def.productionSpec.definition.operations);
|
|
2341
|
+
this.classDefs = { personnel: def.personnelClasses, equipment: def.equipmentClasses, asset: def.assetClasses, material: def.materialClasses };
|
|
2342
|
+
this.materialDefs = new Map((def.materialDefinitions ?? []).filter((d) => d?.id).map((d) => [d.id, d]));
|
|
2343
|
+
const sync = (map, declared, build) => {
|
|
2344
|
+
const ids = new Set(declared.map((d) => d.id));
|
|
2345
|
+
let dropped = 0;
|
|
2346
|
+
for (const id of [...map.keys()]) if (!ids.has(id)) {
|
|
2347
|
+
map.delete(id);
|
|
2348
|
+
dropped++;
|
|
2297
2349
|
}
|
|
2298
|
-
|
|
2350
|
+
for (const d of declared) if (!map.has(d.id)) map.set(d.id, build(d));
|
|
2351
|
+
return dropped;
|
|
2352
|
+
};
|
|
2353
|
+
const declaredLocations = readBoardLocations(def);
|
|
2354
|
+
const locationsBefore = this.locations.size;
|
|
2355
|
+
const locationsDropped = sync(this.locations, declaredLocations, (n) => this.buildLocation(n));
|
|
2356
|
+
const equipmentDropped = sync(this.equipment, readBoardEquipment(def), (m) => this.buildEquipment(m));
|
|
2357
|
+
const personsDropped = sync(this.persons, def.persons ?? [], (p) => this.buildPerson(p));
|
|
2358
|
+
const assetsDropped = sync(this.assets, readBoardAssets(def), (a) => this.buildAsset(a));
|
|
2359
|
+
const observed = this.observer?.adoptStructure(def);
|
|
2360
|
+
if (this.observer) this.observedDirty = true;
|
|
2361
|
+
this.revision++;
|
|
2362
|
+
return {
|
|
2363
|
+
locationsAdded: observed?.locationsAdded ?? Math.max(0, this.locations.size - locationsBefore + locationsDropped),
|
|
2364
|
+
locationsDropped: observed?.locationsDropped ?? locationsDropped,
|
|
2365
|
+
equipmentDropped,
|
|
2366
|
+
personsDropped,
|
|
2367
|
+
assetsDropped
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2370
|
+
buildEquipment(m) {
|
|
2371
|
+
const eq = { id: m.id, kind: m.kind, location: m.homeLocation, homeLocation: m.homeLocation, ...m.properties ? { properties: m.properties } : {}, ...m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}, status: "idle", taskId: null, runMs: 0, setupMs: 0, downMs: 0, goodCount: 0, scrapCount: 0, window: m.window, ...m.workCalendar ? { workCalendar: m.workCalendar } : {}, ...effectiveOnly(m) };
|
|
2372
|
+
if (m.mtbfMs !== void 0) {
|
|
2373
|
+
eq.mtbfMs = m.mtbfMs;
|
|
2374
|
+
eq.mttrMs = m.mttrMs;
|
|
2375
|
+
eq.nextFailureMs = this.sampleExp(m.mtbfMs);
|
|
2299
2376
|
}
|
|
2377
|
+
return eq;
|
|
2300
2378
|
}
|
|
2301
2379
|
/**
|
|
2302
2380
|
* what-if 구성 변주 — fork(또는 실행 중) 엔진에 설비 추가. loadTwinModel 설비 삽입과 동일 규약.
|
package/package.json
CHANGED