@operato/twin-kernel 0.7.49 → 0.7.50
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/flow-engine.d.ts +33 -3
- package/dist/flow-engine.js +116 -19
- package/dist-cjs/index.cjs +58 -19
- package/package.json +1 -1
package/dist/flow-engine.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TestResult, ISOTime, MaterialQuantity, WorkCalendarEntry, EffectivePeriod, Effectivity, OffCalendarReason, ResourceProperty, ResourceClassDef, MaterialDefinition, Attention, TwinModelDef, CanonicalEnvelope, Command, CommandAck, EventHandler, EquipmentMotion, OeeMetrics, AssetState, GeneratorSpec, InterventionOutcome, OrderState, PersonState, ScenarioControl, ScenarioOverride, StateSnapshot, TwinKernel, Unsubscribe, LocationState, ItemState, EquipmentState, OrderStatusDelta, TaskState, StructureShift, IdentityGroundingView, IdentityDeclaration } from './contract.ts';
|
|
1
|
+
import type { TestResult, ISOTime, MaterialQuantity, WorkCalendarEntry, EffectivePeriod, Effectivity, OffCalendarReason, ResourceProperty, ResourceClassDef, MaterialDefinition, Attention, TwinModelDef, CanonicalEnvelope, Command, CommandAck, EventHandler, EquipmentMotion, OeeMetrics, AssetState, GeneratorSpec, InterventionOutcome, OrderState, PersonState, ScenarioControl, ScenarioOverride, StateSnapshot, TwinKernel, Unsubscribe, LocationState, ItemState, EquipmentState, OrderStatusDelta, TaskState, TaskStatus, StructureShift, IdentityGroundingView, IdentityDeclaration } 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';
|
|
@@ -130,7 +130,18 @@ export interface FlowEquipment extends EffectivePeriod {
|
|
|
130
130
|
export interface FlowTask {
|
|
131
131
|
id: string;
|
|
132
132
|
kind: string;
|
|
133
|
-
|
|
133
|
+
/**
|
|
134
|
+
* **작업 상태 — 어휘는 하나다**(`TaskStatus`: created · assigned · in-progress · completed).
|
|
135
|
+
*
|
|
136
|
+
* 예전에는 여기만 셋이었다(`assigned` 없음). 시뮬레이션은 배정과 착수를 한 번에 하므로 그것으로
|
|
137
|
+
* 충분했지만, **미러는 원본이 말한 것을 그대로 들고 있어야 한다** — 그리고 주의 계산은 이미
|
|
138
|
+
* `assigned` 를 기다리고 있었다(§`collectAttentions`: `status === 'created' || status === 'assigned'`).
|
|
139
|
+
* 즉 그 자리는 영원히 오지 않는 값을 기다렸다.
|
|
140
|
+
*
|
|
141
|
+
* 시뮬 경로의 거동은 바뀌지 않는다: 시뮬은 `assigned` 를 만들지 않고, 배정 루프는 `created` 만
|
|
142
|
+
* 집는다(§`assign`). 넓힌 것은 **관측이 말한 사실을 담을 자리**다.
|
|
143
|
+
*/
|
|
144
|
+
status: TaskStatus;
|
|
134
145
|
/**
|
|
135
146
|
* **실제로 들어가고 나온 자재** — ISA-95 `JobResponse.MaterialActual`.
|
|
136
147
|
*
|
|
@@ -700,7 +711,26 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
700
711
|
id: string;
|
|
701
712
|
since: string;
|
|
702
713
|
}[];
|
|
703
|
-
}, orders?: OrderStatusDelta[]
|
|
714
|
+
}, orders?: OrderStatusDelta[],
|
|
715
|
+
/**
|
|
716
|
+
* **무엇을 하러 심나** — 기본은 씨앗이다(외부 호출자 전부가 그것이다).
|
|
717
|
+
*
|
|
718
|
+
* ── 왜 이 구별이 생겼나 (2026-08-22 실측) ─────────────────────────────────
|
|
719
|
+
* 이 함수는 원래 **스냅샷으로 커널을 세우는 문**이다(웜스타트·fork·예측). 그 목적에서는 딛고 설
|
|
720
|
+
* 물품·오더가 없는 작업을 빼는 것이 옳다 — 굴릴 수 없는 작업을 심으면 첫 틱에서 없는 자리를
|
|
721
|
+
* 가리킨다.
|
|
722
|
+
*
|
|
723
|
+
* 그런데 `settleObserved()` 가 **미러 자신의 관측을 옮길 때도** 같은 함수를 쓴다. 목적이 다르다:
|
|
724
|
+
* 미러는 그 작업을 굴리지 않는다 — **들은 것을 말할 뿐이다.** 그런데 규칙이 하나였으므로 관측된
|
|
725
|
+
* 사실이 씨앗 규칙에 걸려 사라졌다.
|
|
726
|
+
*
|
|
727
|
+
* 실측: 포천 미러의 저널에 `task.status` 5,786건이 있고 투영기로 접으면 작업 2,881건인데
|
|
728
|
+
* `getSnapshot().tasks` 는 **0** 이었다. 화면은 「아직 하나도 없습니다」라고 말했고 사용자가
|
|
729
|
+
* 반나절을 찾았다. 재현하면 관측기 5건 → 커널 0건이고, 걸린 조건은 오더였다.
|
|
730
|
+
*
|
|
731
|
+
* 그래서 규칙을 둘로 쪼개지 않고 **목적을 밝힌다.** 같은 함수, 같은 주입 경로, 다른 판정 하나다.
|
|
732
|
+
*/
|
|
733
|
+
purpose?: 'seed' | 'observe'): void;
|
|
704
734
|
/**
|
|
705
735
|
* what-if 구성 변주 — **선언을 덮어쓴다**(fork 대상). 바꿨으면 true.
|
|
706
736
|
*
|
package/dist/flow-engine.js
CHANGED
|
@@ -832,7 +832,26 @@ export class FlowEngine {
|
|
|
832
832
|
this.seedDanglingRefs += dropped;
|
|
833
833
|
return { kept, dropped };
|
|
834
834
|
}
|
|
835
|
-
hydrateObserved(snap, orders = []
|
|
835
|
+
hydrateObserved(snap, orders = [],
|
|
836
|
+
/**
|
|
837
|
+
* **무엇을 하러 심나** — 기본은 씨앗이다(외부 호출자 전부가 그것이다).
|
|
838
|
+
*
|
|
839
|
+
* ── 왜 이 구별이 생겼나 (2026-08-22 실측) ─────────────────────────────────
|
|
840
|
+
* 이 함수는 원래 **스냅샷으로 커널을 세우는 문**이다(웜스타트·fork·예측). 그 목적에서는 딛고 설
|
|
841
|
+
* 물품·오더가 없는 작업을 빼는 것이 옳다 — 굴릴 수 없는 작업을 심으면 첫 틱에서 없는 자리를
|
|
842
|
+
* 가리킨다.
|
|
843
|
+
*
|
|
844
|
+
* 그런데 `settleObserved()` 가 **미러 자신의 관측을 옮길 때도** 같은 함수를 쓴다. 목적이 다르다:
|
|
845
|
+
* 미러는 그 작업을 굴리지 않는다 — **들은 것을 말할 뿐이다.** 그런데 규칙이 하나였으므로 관측된
|
|
846
|
+
* 사실이 씨앗 규칙에 걸려 사라졌다.
|
|
847
|
+
*
|
|
848
|
+
* 실측: 포천 미러의 저널에 `task.status` 5,786건이 있고 투영기로 접으면 작업 2,881건인데
|
|
849
|
+
* `getSnapshot().tasks` 는 **0** 이었다. 화면은 「아직 하나도 없습니다」라고 말했고 사용자가
|
|
850
|
+
* 반나절을 찾았다. 재현하면 관측기 5건 → 커널 0건이고, 걸린 조건은 오더였다.
|
|
851
|
+
*
|
|
852
|
+
* 그래서 규칙을 둘로 쪼개지 않고 **목적을 밝힌다.** 같은 함수, 같은 주입 경로, 다른 판정 하나다.
|
|
853
|
+
*/
|
|
854
|
+
purpose = 'seed') {
|
|
836
855
|
/* 확인 처리를 먼저 이어받는다 — 아래에서 상태를 주입하면 곧바로 주목 신호가 계산되므로, 늦게
|
|
837
856
|
* 이어받으면 그 한 번은 확인 안 된 것으로 계산된다(화면이 잠깐 빨개진다). */
|
|
838
857
|
for (const id of snap.acked ?? [])
|
|
@@ -882,7 +901,15 @@ export class FlowEngine {
|
|
|
882
901
|
const prev = this.equipment.get(m.id);
|
|
883
902
|
this.equipment.set(m.id, {
|
|
884
903
|
...(prev ?? {}),
|
|
885
|
-
|
|
904
|
+
/*
|
|
905
|
+
* `taskId` — **관측이면 원본이 말한 값이다.**
|
|
906
|
+
*
|
|
907
|
+
* 씨앗에서는 `null` 로 두고 작업 복원이 다시 세운다(작업이 함께 오지 않으면 그 자원이 영원히
|
|
908
|
+
* 잡혀 있게 되므로). 그런데 관측에서는 `equipment.status` 가 이 값을 **직접 실어 온다** —
|
|
909
|
+
* 상태(busy)는 그 축에서 받고 묶임은 작업에서 되세우면, 같은 사실의 두 조각이 서로 다른 축에서
|
|
910
|
+
* 오게 되고 한쪽만 도착한 순간 어긋난다.
|
|
911
|
+
*/
|
|
912
|
+
id: m.id, kind: m.kind, location: m.location ?? '', ...(m.homeLocation ? { homeLocation: m.homeLocation } : {}), status: m.status ?? 'idle', taskId: purpose === 'observe' ? (m.taskId ?? null) : null,
|
|
886
913
|
runMs: oee?.runMs ?? 0, setupMs: oee?.setupMs ?? 0, downMs: oee?.downMs ?? 0,
|
|
887
914
|
goodCount: oee?.goodCount ?? 0, scrapCount: oee?.scrapCount ?? 0,
|
|
888
915
|
/*
|
|
@@ -915,7 +942,10 @@ export class FlowEngine {
|
|
|
915
942
|
const prev = this.persons.get(p.id);
|
|
916
943
|
this.persons.set(p.id, {
|
|
917
944
|
...(prev ?? {}),
|
|
918
|
-
|
|
945
|
+
/* 설비와 같은 규율 — 관측이면 원본이 말한 상태·묶임을 지킨다(씨앗이면 작업 복원이 세운다). */
|
|
946
|
+
id: p.id, personnelClassIds: p.personnelClassIds ?? prev?.personnelClassIds,
|
|
947
|
+
status: purpose === 'observe' ? (p.status ?? 'idle') : 'idle',
|
|
948
|
+
taskId: purpose === 'observe' ? (p.taskId ?? null) : null,
|
|
919
949
|
...(p.location ? { location: p.location } : {}),
|
|
920
950
|
...(p.properties ? { properties: p.properties } : {}),
|
|
921
951
|
...(p.testSpecificationIds ? { testSpecificationIds: p.testSpecificationIds } : {}),
|
|
@@ -929,7 +959,10 @@ export class FlowEngine {
|
|
|
929
959
|
const prev = this.assets.get(a.id);
|
|
930
960
|
this.assets.set(a.id, {
|
|
931
961
|
...(prev ?? {}),
|
|
932
|
-
|
|
962
|
+
/* 설비·사람과 같은 규율 — 관측이면 원본이 말한 상태·묶임을 지킨다. */
|
|
963
|
+
id: a.id, assetClassIds: a.assetClassIds ?? prev?.assetClassIds, location: a.location,
|
|
964
|
+
status: purpose === 'observe' ? (a.status ?? 'idle') : 'idle',
|
|
965
|
+
taskId: purpose === 'observe' ? (a.taskId ?? null) : null, carrying: a.carrying,
|
|
933
966
|
...(a.properties ? { properties: a.properties } : {}),
|
|
934
967
|
...(a.testSpecificationIds ? { testSpecificationIds: a.testSpecificationIds } : {}),
|
|
935
968
|
/* 결과도 이어받는다 — 잃으면 예측이 **자격 만료를 모르는 현장**에서 출발한다(낙관 쪽으로 치우친다). */
|
|
@@ -1022,38 +1055,83 @@ export class FlowEngine {
|
|
|
1022
1055
|
}
|
|
1023
1056
|
/* 작업이 딛고 설 오더를 먼저 세운다 — 순서가 뒤바뀌면 아래 확인이 언제나 "없다" 로 답한다. */
|
|
1024
1057
|
const seededOrderIds = new Set(this.orders.keys());
|
|
1025
|
-
|
|
1058
|
+
/*
|
|
1059
|
+
* **떨어뜨린 이유를 따로 센다** — 예전에는 하나로 세고 「whose item is no longer in state」라고
|
|
1060
|
+
* 말했다. 그런데 오더에 걸린 경우도 같은 문장을 썼고, 그것을 본 사람이 물품 쪽을 반나절 뒤졌다.
|
|
1061
|
+
* 원인을 지어내는 문장은 없는 것보다 나쁘다.
|
|
1062
|
+
*/
|
|
1063
|
+
let droppedNoItem = 0;
|
|
1064
|
+
let droppedNoOrder = 0;
|
|
1065
|
+
let droppedCompleted = 0;
|
|
1066
|
+
/*
|
|
1067
|
+
* **씨앗이냐 관측 정착이냐** — 아래 셋을 거를지 말지가 여기서 갈린다(§`purpose`).
|
|
1068
|
+
*
|
|
1069
|
+
* 씨앗(seed) 굴릴 수 없는 작업은 뺀다. 심으면 첫 틱에서 없는 자리를 가리킨다.
|
|
1070
|
+
* 관측(observe) **거르지 않는다.** 미러는 그 작업을 굴리지 않고, 들은 것을 말한다.
|
|
1071
|
+
*/
|
|
1072
|
+
const filtering = purpose === 'seed';
|
|
1026
1073
|
for (const t of snap.tasks ?? []) {
|
|
1027
|
-
if (t.status === 'completed')
|
|
1074
|
+
if (filtering && t.status === 'completed') {
|
|
1075
|
+
droppedCompleted++;
|
|
1028
1076
|
continue;
|
|
1077
|
+
}
|
|
1029
1078
|
const ref = t.itemRefs?.[0];
|
|
1030
|
-
if (ref && !this.itemByRef(ref)) {
|
|
1031
|
-
|
|
1079
|
+
if (filtering && ref && !this.itemByRef(ref)) {
|
|
1080
|
+
droppedNoItem++;
|
|
1032
1081
|
continue;
|
|
1033
1082
|
}
|
|
1034
1083
|
/* 오더도 같다 — 이미 이행된 오더는 주입하지 않으므로(위 `remaining <= 0`), 그 오더에 딸린 작업만
|
|
1035
1084
|
남으면 완료 시점에 없는 오더를 딛는다. 주입 단계에서 함께 뺀다. */
|
|
1036
|
-
if (t.orderId && !seededOrderIds.has(t.orderId)) {
|
|
1037
|
-
|
|
1085
|
+
if (filtering && t.orderId && !seededOrderIds.has(t.orderId)) {
|
|
1086
|
+
droppedNoOrder++;
|
|
1038
1087
|
continue;
|
|
1039
1088
|
}
|
|
1040
1089
|
const known = typeof t.remainingMs === 'number' && Number.isFinite(t.remainingMs);
|
|
1041
1090
|
this.tasks.set(t.id, {
|
|
1042
1091
|
id: t.id, kind: t.kind,
|
|
1043
|
-
|
|
1092
|
+
/*
|
|
1093
|
+
* **관측이면 들은 상태를 그대로 둔다.** 씨앗이면 굴릴 수 있는 모양으로 맞춘다(남은 시간을
|
|
1094
|
+
* 모르는 작업을 `in-progress` 로 심으면 끝나지 않는다).
|
|
1095
|
+
*
|
|
1096
|
+
* 예전에는 관측 경로에서도 이 줄이 상태를 다시 썼다 — 원본이 `completed` 라고 말한 작업이
|
|
1097
|
+
* `created` 로, `assigned` 도 `created` 로 바뀌었다. 공정 타임라인·현장 성과·주의 목록이
|
|
1098
|
+
* 모두 이 축을 읽으므로, 여기서 뭉개면 그 셋이 함께 거짓이 된다.
|
|
1099
|
+
*/
|
|
1100
|
+
status: purpose === 'observe' ? t.status : known && t.status === 'in-progress' ? 'in-progress' : 'created',
|
|
1044
1101
|
/* 이미 일어난 자재 이동은 **씨앗에도 남는다** — 잃으면 실적이 재기동마다 지워진다. */
|
|
1045
1102
|
...(t.materialActual?.length ? { materialActual: t.materialActual.map(r => ({ ...r })) } : {}),
|
|
1046
1103
|
itemEpc: t.itemRefs?.[0] ?? '',
|
|
1047
1104
|
fromNode: t.fromNode ?? '', toNode: t.toNode ?? '',
|
|
1048
|
-
|
|
1105
|
+
/*
|
|
1106
|
+
* 자원 참조는 **작업 자신의 사실**이다. 씨앗에서는 굴릴 수 있는 작업만 자원을 잡게 하지만
|
|
1107
|
+
* (남은 시간을 모르면 배정 루프가 다시 잡아야 한다), 관측에서는 원본이 말한 참조를 지운 이유가
|
|
1108
|
+
* 없다 — 지우면 「어느 설비가 이 일을 하고 있나」가 화면에서 사라진다.
|
|
1109
|
+
*/
|
|
1110
|
+
resource: purpose === 'observe' ? (t.resourceRef ?? null) : known && t.status === 'in-progress' ? (t.resourceRef ?? null) : null,
|
|
1049
1111
|
remainingMs: known ? t.remainingMs : (t.durationMs ?? 0),
|
|
1050
1112
|
startedAtSimMs: t.startedAtSimMs,
|
|
1051
1113
|
durationMs: t.durationMs ?? (known ? t.remainingMs : 0),
|
|
1052
1114
|
orderId: t.orderId,
|
|
1053
1115
|
intent: t.intent
|
|
1054
1116
|
});
|
|
1055
|
-
/*
|
|
1056
|
-
|
|
1117
|
+
/*
|
|
1118
|
+
* 투입 인원은 **작업이 들고 있는 사실**이므로 관측에서도 그대로 옮긴다(위 `set` 에서 빠져 있어
|
|
1119
|
+
* 아래에서 채운다 — 씨앗 시절 진행 중인 작업에만 필요했던 자리다).
|
|
1120
|
+
*/
|
|
1121
|
+
if (purpose === 'observe' && t.personnel?.length) {
|
|
1122
|
+
const restored = this.tasks.get(t.id);
|
|
1123
|
+
if (restored)
|
|
1124
|
+
restored.personnel = [...t.personnel];
|
|
1125
|
+
}
|
|
1126
|
+
/*
|
|
1127
|
+
* **자원의 상태는 여기서 정하지 않는다 — 관측일 때는.**
|
|
1128
|
+
*
|
|
1129
|
+
* 씨앗에서는 필요하다: 진행 중인 작업을 심었으면 그 설비가 다른 일을 또 받으면 안 되므로 점유를
|
|
1130
|
+
* 표시한다. 그런데 미러에서는 설비·사람의 상태가 **자기 관측 축**(`equipment.status`,
|
|
1131
|
+
* `person.status`)에서 온다. 작업 하나로 그것을 덮으면 원본이 「유휴」라고 말한 설비를 트윈이
|
|
1132
|
+
* 「바쁘다」고 말한다 — 축이 둘이 되고, 어느 쪽이 사실인지 알 수 없게 된다.
|
|
1133
|
+
*/
|
|
1134
|
+
if (purpose === 'seed' && known && t.status === 'in-progress' && t.resourceRef) {
|
|
1057
1135
|
const mv = this.equipment.get(t.resourceRef);
|
|
1058
1136
|
if (mv) {
|
|
1059
1137
|
mv.status = 'busy';
|
|
@@ -1061,7 +1139,7 @@ export class FlowEngine {
|
|
|
1061
1139
|
}
|
|
1062
1140
|
}
|
|
1063
1141
|
/* 사람도 같다 — 진행 중이던 작업에 투입돼 있던 사람은 여전히 점유되어 있어야 한다. */
|
|
1064
|
-
if (known && t.status === 'in-progress') {
|
|
1142
|
+
if (purpose === 'seed' && known && t.status === 'in-progress') {
|
|
1065
1143
|
const restored = this.tasks.get(t.id);
|
|
1066
1144
|
if (restored)
|
|
1067
1145
|
restored.personnel = t.personnel ? [...t.personnel] : undefined;
|
|
@@ -1074,9 +1152,21 @@ export class FlowEngine {
|
|
|
1074
1152
|
}
|
|
1075
1153
|
}
|
|
1076
1154
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1155
|
+
/*
|
|
1156
|
+
* **무엇을 왜 뺐는지 그대로 말한다.** 씨앗에서만 나온다(관측은 빼지 않는다).
|
|
1157
|
+
*
|
|
1158
|
+
* 예전 문장은 셋을 하나로 뭉쳐 「whose item is no longer in state」라고 했다. 오더에 걸린 것을
|
|
1159
|
+
* 물품 탓으로 말했고, 그 문장을 읽은 사람이 물품 쪽을 뒤졌다. 원인을 지어내는 문장은 침묵보다 나쁘다.
|
|
1160
|
+
*/
|
|
1161
|
+
const droppedTotal = droppedNoItem + droppedNoOrder + droppedCompleted;
|
|
1162
|
+
if (droppedTotal) {
|
|
1163
|
+
const why = [
|
|
1164
|
+
droppedNoItem ? `${droppedNoItem} whose item is not in state` : '',
|
|
1165
|
+
droppedNoOrder ? `${droppedNoOrder} whose order is not in state` : '',
|
|
1166
|
+
droppedCompleted ? `${droppedCompleted} already completed` : ''
|
|
1167
|
+
].filter(Boolean).join(' · ');
|
|
1168
|
+
console.warn(`[twin-kernel] seed skipped ${droppedTotal} task(s) — ${why}. ` +
|
|
1169
|
+
'They cannot be continued by a simulation. (A mirror keeps them: hydrateObserved(…, …, "observe").)');
|
|
1080
1170
|
}
|
|
1081
1171
|
}
|
|
1082
1172
|
/**
|
|
@@ -1723,7 +1813,14 @@ export class FlowEngine {
|
|
|
1723
1813
|
if (!this.observedDirty || !this.observer)
|
|
1724
1814
|
return;
|
|
1725
1815
|
this.observedDirty = false;
|
|
1726
|
-
|
|
1816
|
+
/*
|
|
1817
|
+
* **목적을 밝힌다 — 이것은 씨앗이 아니라 관측 정착이다**(§`hydrateObserved` 의 `purpose`).
|
|
1818
|
+
*
|
|
1819
|
+
* 이 한 낱말이 없던 동안 미러의 작업이 씨앗 규칙에 걸려 사라졌다: 포천 미러의 저널에
|
|
1820
|
+
* `task.status` 5,786건이 있는데 `getSnapshot().tasks` 는 0 이었고, 화면은 「아직 하나도
|
|
1821
|
+
* 없습니다」라고 말했다. 미러는 그 작업을 굴리지 않는다 — 들은 것을 말할 뿐이다.
|
|
1822
|
+
*/
|
|
1823
|
+
this.hydrateObserved(this.observer.snapshot(), [], 'observe');
|
|
1727
1824
|
}
|
|
1728
1825
|
/** 구독자 목록 — 관측 재방출용(private handlers 에 접근). */
|
|
1729
1826
|
observedHandlers() {
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -3964,7 +3964,7 @@ var FlowEngine = class {
|
|
|
3964
3964
|
this.seedDanglingRefs += dropped;
|
|
3965
3965
|
return { kept, dropped };
|
|
3966
3966
|
}
|
|
3967
|
-
hydrateObserved(snap, orders = []) {
|
|
3967
|
+
hydrateObserved(snap, orders = [], purpose = "seed") {
|
|
3968
3968
|
for (const id of snap.acked ?? []) this._acked.add(id);
|
|
3969
3969
|
for (const e of snap.attentionSince ?? []) if (e?.id && e.since) this._attentionSince.set(e.id, e.since);
|
|
3970
3970
|
for (const n of snap.locations) {
|
|
@@ -3994,12 +3994,20 @@ var FlowEngine = class {
|
|
|
3994
3994
|
const prev = this.equipment.get(m.id);
|
|
3995
3995
|
this.equipment.set(m.id, {
|
|
3996
3996
|
...prev ?? {},
|
|
3997
|
+
/*
|
|
3998
|
+
* `taskId` — **관측이면 원본이 말한 값이다.**
|
|
3999
|
+
*
|
|
4000
|
+
* 씨앗에서는 `null` 로 두고 작업 복원이 다시 세운다(작업이 함께 오지 않으면 그 자원이 영원히
|
|
4001
|
+
* 잡혀 있게 되므로). 그런데 관측에서는 `equipment.status` 가 이 값을 **직접 실어 온다** —
|
|
4002
|
+
* 상태(busy)는 그 축에서 받고 묶임은 작업에서 되세우면, 같은 사실의 두 조각이 서로 다른 축에서
|
|
4003
|
+
* 오게 되고 한쪽만 도착한 순간 어긋난다.
|
|
4004
|
+
*/
|
|
3997
4005
|
id: m.id,
|
|
3998
4006
|
kind: m.kind,
|
|
3999
4007
|
location: m.location ?? "",
|
|
4000
4008
|
...m.homeLocation ? { homeLocation: m.homeLocation } : {},
|
|
4001
4009
|
status: m.status ?? "idle",
|
|
4002
|
-
taskId: null,
|
|
4010
|
+
taskId: purpose === "observe" ? m.taskId ?? null : null,
|
|
4003
4011
|
runMs: oee?.runMs ?? 0,
|
|
4004
4012
|
setupMs: oee?.setupMs ?? 0,
|
|
4005
4013
|
downMs: oee?.downMs ?? 0,
|
|
@@ -4030,10 +4038,11 @@ var FlowEngine = class {
|
|
|
4030
4038
|
const prev = this.persons.get(p.id);
|
|
4031
4039
|
this.persons.set(p.id, {
|
|
4032
4040
|
...prev ?? {},
|
|
4041
|
+
/* 설비와 같은 규율 — 관측이면 원본이 말한 상태·묶임을 지킨다(씨앗이면 작업 복원이 세운다). */
|
|
4033
4042
|
id: p.id,
|
|
4034
4043
|
personnelClassIds: p.personnelClassIds ?? prev?.personnelClassIds,
|
|
4035
|
-
status: "idle",
|
|
4036
|
-
taskId: null,
|
|
4044
|
+
status: purpose === "observe" ? p.status ?? "idle" : "idle",
|
|
4045
|
+
taskId: purpose === "observe" ? p.taskId ?? null : null,
|
|
4037
4046
|
...p.location ? { location: p.location } : {},
|
|
4038
4047
|
...p.properties ? { properties: p.properties } : {},
|
|
4039
4048
|
...p.testSpecificationIds ? { testSpecificationIds: p.testSpecificationIds } : {},
|
|
@@ -4046,11 +4055,12 @@ var FlowEngine = class {
|
|
|
4046
4055
|
const prev = this.assets.get(a.id);
|
|
4047
4056
|
this.assets.set(a.id, {
|
|
4048
4057
|
...prev ?? {},
|
|
4058
|
+
/* 설비·사람과 같은 규율 — 관측이면 원본이 말한 상태·묶임을 지킨다. */
|
|
4049
4059
|
id: a.id,
|
|
4050
4060
|
assetClassIds: a.assetClassIds ?? prev?.assetClassIds,
|
|
4051
4061
|
location: a.location,
|
|
4052
|
-
status: "idle",
|
|
4053
|
-
taskId: null,
|
|
4062
|
+
status: purpose === "observe" ? a.status ?? "idle" : "idle",
|
|
4063
|
+
taskId: purpose === "observe" ? a.taskId ?? null : null,
|
|
4054
4064
|
carrying: a.carrying,
|
|
4055
4065
|
...a.properties ? { properties: a.properties } : {},
|
|
4056
4066
|
...a.testSpecificationIds ? { testSpecificationIds: a.testSpecificationIds } : {},
|
|
@@ -4119,43 +4129,66 @@ var FlowEngine = class {
|
|
|
4119
4129
|
});
|
|
4120
4130
|
}
|
|
4121
4131
|
const seededOrderIds = new Set(this.orders.keys());
|
|
4122
|
-
let
|
|
4132
|
+
let droppedNoItem = 0;
|
|
4133
|
+
let droppedNoOrder = 0;
|
|
4134
|
+
let droppedCompleted = 0;
|
|
4135
|
+
const filtering = purpose === "seed";
|
|
4123
4136
|
for (const t of snap.tasks ?? []) {
|
|
4124
|
-
if (t.status === "completed")
|
|
4137
|
+
if (filtering && t.status === "completed") {
|
|
4138
|
+
droppedCompleted++;
|
|
4139
|
+
continue;
|
|
4140
|
+
}
|
|
4125
4141
|
const ref = t.itemRefs?.[0];
|
|
4126
|
-
if (ref && !this.itemByRef(ref)) {
|
|
4127
|
-
|
|
4142
|
+
if (filtering && ref && !this.itemByRef(ref)) {
|
|
4143
|
+
droppedNoItem++;
|
|
4128
4144
|
continue;
|
|
4129
4145
|
}
|
|
4130
|
-
if (t.orderId && !seededOrderIds.has(t.orderId)) {
|
|
4131
|
-
|
|
4146
|
+
if (filtering && t.orderId && !seededOrderIds.has(t.orderId)) {
|
|
4147
|
+
droppedNoOrder++;
|
|
4132
4148
|
continue;
|
|
4133
4149
|
}
|
|
4134
4150
|
const known = typeof t.remainingMs === "number" && Number.isFinite(t.remainingMs);
|
|
4135
4151
|
this.tasks.set(t.id, {
|
|
4136
4152
|
id: t.id,
|
|
4137
4153
|
kind: t.kind,
|
|
4138
|
-
|
|
4154
|
+
/*
|
|
4155
|
+
* **관측이면 들은 상태를 그대로 둔다.** 씨앗이면 굴릴 수 있는 모양으로 맞춘다(남은 시간을
|
|
4156
|
+
* 모르는 작업을 `in-progress` 로 심으면 끝나지 않는다).
|
|
4157
|
+
*
|
|
4158
|
+
* 예전에는 관측 경로에서도 이 줄이 상태를 다시 썼다 — 원본이 `completed` 라고 말한 작업이
|
|
4159
|
+
* `created` 로, `assigned` 도 `created` 로 바뀌었다. 공정 타임라인·현장 성과·주의 목록이
|
|
4160
|
+
* 모두 이 축을 읽으므로, 여기서 뭉개면 그 셋이 함께 거짓이 된다.
|
|
4161
|
+
*/
|
|
4162
|
+
status: purpose === "observe" ? t.status : known && t.status === "in-progress" ? "in-progress" : "created",
|
|
4139
4163
|
/* 이미 일어난 자재 이동은 **씨앗에도 남는다** — 잃으면 실적이 재기동마다 지워진다. */
|
|
4140
4164
|
...t.materialActual?.length ? { materialActual: t.materialActual.map((r) => ({ ...r })) } : {},
|
|
4141
4165
|
itemEpc: t.itemRefs?.[0] ?? "",
|
|
4142
4166
|
fromNode: t.fromNode ?? "",
|
|
4143
4167
|
toNode: t.toNode ?? "",
|
|
4144
|
-
|
|
4168
|
+
/*
|
|
4169
|
+
* 자원 참조는 **작업 자신의 사실**이다. 씨앗에서는 굴릴 수 있는 작업만 자원을 잡게 하지만
|
|
4170
|
+
* (남은 시간을 모르면 배정 루프가 다시 잡아야 한다), 관측에서는 원본이 말한 참조를 지운 이유가
|
|
4171
|
+
* 없다 — 지우면 「어느 설비가 이 일을 하고 있나」가 화면에서 사라진다.
|
|
4172
|
+
*/
|
|
4173
|
+
resource: purpose === "observe" ? t.resourceRef ?? null : known && t.status === "in-progress" ? t.resourceRef ?? null : null,
|
|
4145
4174
|
remainingMs: known ? t.remainingMs : t.durationMs ?? 0,
|
|
4146
4175
|
startedAtSimMs: t.startedAtSimMs,
|
|
4147
4176
|
durationMs: t.durationMs ?? (known ? t.remainingMs : 0),
|
|
4148
4177
|
orderId: t.orderId,
|
|
4149
4178
|
intent: t.intent
|
|
4150
4179
|
});
|
|
4151
|
-
if (
|
|
4180
|
+
if (purpose === "observe" && t.personnel?.length) {
|
|
4181
|
+
const restored = this.tasks.get(t.id);
|
|
4182
|
+
if (restored) restored.personnel = [...t.personnel];
|
|
4183
|
+
}
|
|
4184
|
+
if (purpose === "seed" && known && t.status === "in-progress" && t.resourceRef) {
|
|
4152
4185
|
const mv = this.equipment.get(t.resourceRef);
|
|
4153
4186
|
if (mv) {
|
|
4154
4187
|
mv.status = "busy";
|
|
4155
4188
|
mv.taskId = t.id;
|
|
4156
4189
|
}
|
|
4157
4190
|
}
|
|
4158
|
-
if (known && t.status === "in-progress") {
|
|
4191
|
+
if (purpose === "seed" && known && t.status === "in-progress") {
|
|
4159
4192
|
const restored = this.tasks.get(t.id);
|
|
4160
4193
|
if (restored) restored.personnel = t.personnel ? [...t.personnel] : void 0;
|
|
4161
4194
|
for (const id of t.personnel ?? []) {
|
|
@@ -4167,9 +4200,15 @@ var FlowEngine = class {
|
|
|
4167
4200
|
}
|
|
4168
4201
|
}
|
|
4169
4202
|
}
|
|
4170
|
-
|
|
4203
|
+
const droppedTotal = droppedNoItem + droppedNoOrder + droppedCompleted;
|
|
4204
|
+
if (droppedTotal) {
|
|
4205
|
+
const why = [
|
|
4206
|
+
droppedNoItem ? `${droppedNoItem} whose item is not in state` : "",
|
|
4207
|
+
droppedNoOrder ? `${droppedNoOrder} whose order is not in state` : "",
|
|
4208
|
+
droppedCompleted ? `${droppedCompleted} already completed` : ""
|
|
4209
|
+
].filter(Boolean).join(" \xB7 ");
|
|
4171
4210
|
console.warn(
|
|
4172
|
-
`[twin-kernel] seed skipped ${
|
|
4211
|
+
`[twin-kernel] seed skipped ${droppedTotal} task(s) \u2014 ${why}. They cannot be continued by a simulation. (A mirror keeps them: hydrateObserved(\u2026, \u2026, "observe").)`
|
|
4173
4212
|
);
|
|
4174
4213
|
}
|
|
4175
4214
|
}
|
|
@@ -4752,7 +4791,7 @@ var FlowEngine = class {
|
|
|
4752
4791
|
settleObserved() {
|
|
4753
4792
|
if (!this.observedDirty || !this.observer) return;
|
|
4754
4793
|
this.observedDirty = false;
|
|
4755
|
-
this.hydrateObserved(this.observer.snapshot());
|
|
4794
|
+
this.hydrateObserved(this.observer.snapshot(), [], "observe");
|
|
4756
4795
|
}
|
|
4757
4796
|
/** 구독자 목록 — 관측 재방출용(private handlers 에 접근). */
|
|
4758
4797
|
observedHandlers() {
|
package/package.json
CHANGED