@operato/twin-kernel 0.7.48 → 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/domain-catalog.d.ts +7 -1
- package/dist/domain-catalog.js +39 -0
- package/dist/flow-engine.d.ts +33 -3
- package/dist/flow-engine.js +130 -20
- package/dist-cjs/index.cjs +107 -20
- package/package.json +1 -1
package/dist/domain-catalog.d.ts
CHANGED
|
@@ -70,8 +70,14 @@ export interface TwinAxisInfo {
|
|
|
70
70
|
* 이 칸이 없던 동안 소비처는 `id`·`key`·`gtin` 을 **짐작**했고, 그래서 화면이 실재하는 항목을
|
|
71
71
|
* 「식별자 없음 — 참조할 수 없는 항목」으로 보였다. 값은 있는데 가리킬 수 없다고 말한 것이다.
|
|
72
72
|
* 짐작을 없애고 선언이 답한다 — 축이 사는 자리(`path`)를 선언하는 것과 같은 이유다.
|
|
73
|
+
*
|
|
74
|
+
* ── 여럿을 받는다 (2026-08-22) ────────────────────────────────────────────
|
|
75
|
+
* 정체성이 **한 칸으로 정해지지 않는 축**이 있다. 물품이 그렇다: 직렬 물품은 `epc` 가 유일하지만,
|
|
76
|
+
* 비직렬 로트가 자리마다 나뉘면 개체를 구별하는 것은 `subLotId` 다(같은 로트의 두 부분은 같은 `epc`
|
|
77
|
+
* 를 갖는다 — §`ItemState.subLotId`). 계약이 그 규칙을 말할 수 없으면 소비처가 다시 짐작한다.
|
|
78
|
+
* 그래서 **차례**를 받는다: 앞에서 값이 있는 첫 칸이 이긴다.
|
|
73
79
|
*/
|
|
74
|
-
idField?: string;
|
|
80
|
+
idField?: string | string[];
|
|
75
81
|
/**
|
|
76
82
|
* 이 축을 **어디서 읽나.**
|
|
77
83
|
*
|
package/dist/domain-catalog.js
CHANGED
|
@@ -97,6 +97,27 @@ export const TWIN_AXES = [
|
|
|
97
97
|
standardClass: { isa95: 'OperationsRequest', epcis: 'TransactionEvent' }, systems: LOGISTICS },
|
|
98
98
|
{ axis: 'tasks', label: 'twin.axis.tasks', kind: 'instance', source: 'state', historical: true,
|
|
99
99
|
standardClass: { isa95: 'SegmentResponse', epcis: 'TransformationEvent' }, systems: LOGISTICS },
|
|
100
|
+
/*
|
|
101
|
+
* ── 물품이 축이 아니었다 (2026-08-22) ──────────────────────────────────────
|
|
102
|
+
* 트윈에서 **수가 가장 많은 것**이 물품인데(실측: 엔티티 3,611 중 2,400 · hatio-us 는 2,805) 그것을
|
|
103
|
+
* 가리켜 걸어 들어갈 자리가 없었다. 지도와 집약 태그에는 이미 보이는데 「모델 살펴보기」에는 문이
|
|
104
|
+
* 없었다 — 사용자가 트윈에 가장 자주 묻는 것이 「내 물건이 어디 있나」이므로 그것은 접근 장벽이다.
|
|
105
|
+
*
|
|
106
|
+
* 성격은 `orders`·`tasks` 와 같다: 상태에 살고, 저널에 이력이 있고, 원본이 낸 것을 트윈이 관측한다.
|
|
107
|
+
* 그래서 같은 조합(`instance` · `state` · `historical`)이다.
|
|
108
|
+
*
|
|
109
|
+
* ── 표준 대응 ─────────────────────────────────────────────────────────────
|
|
110
|
+
* ISA-95 는 `MaterialLot` 이다 — 물품은 「무슨 품목인가」(정의)가 아니라 「그 품목의 이 덩어리」이고,
|
|
111
|
+
* 위치·수량·부분(`MaterialSubLot`)을 그 자리가 든다. EPCIS 는 `ObjectEvent` 다: 개체가 생기고
|
|
112
|
+
* 관측되고 사라지는 것을 그 사건이 말한다.
|
|
113
|
+
*
|
|
114
|
+
* ── 무엇이 이 항목을 가리키나 ─────────────────────────────────────────────
|
|
115
|
+
* `subLotId ?? epc` 다. 짐작에 맡기면 `gtin` 으로 떨어지고, 그러면 **같은 품목의 물품 전부가 한
|
|
116
|
+
* 식별자로 뭉친다** — 화면이 2,400개를 몇 개로 보인다.
|
|
117
|
+
*/
|
|
118
|
+
{ axis: 'items', label: 'twin.axis.items', kind: 'instance', source: 'state', historical: true,
|
|
119
|
+
idField: ['subLotId', 'epc'],
|
|
120
|
+
standardClass: { isa95: 'MaterialLot', epcis: 'ObjectEvent' }, systems: LOGISTICS },
|
|
100
121
|
/*
|
|
101
122
|
* ── 에너지가 더하는 개념은 **하나**다 (2026-08-14, §10 6.5단계) ──────────────
|
|
102
123
|
*
|
|
@@ -163,6 +184,24 @@ export const TWIN_RELATIONS = [
|
|
|
163
184
|
{ from: 'tasks', field: 'toNode', target: { kind: 'axis', axis: 'locations' }, via: 'twin.rel.at', optional: true },
|
|
164
185
|
{ from: 'tasks', field: 'resourceRef', target: { kind: 'axis', axis: 'equipment' }, via: 'twin.rel.by', optional: true },
|
|
165
186
|
{ from: 'tasks', field: 'personnel[]', target: { kind: 'axis', axis: 'persons' }, via: 'twin.rel.crew', optional: true },
|
|
187
|
+
/*
|
|
188
|
+
* 물품의 관계 (2026-08-22) — 없으면 축이 **걸어 들어갈 수 없는 목록**이 된다.
|
|
189
|
+
*
|
|
190
|
+
* `location` 은 필수다 — 물품은 언제나 어딘가에 있다(그것이 물품의 뜻이다). 나머지는 선택이다:
|
|
191
|
+
* 물류단위에 담기지 않은 물품, 자산에 실리지 않은 팔레트가 정상이다.
|
|
192
|
+
*
|
|
193
|
+
* `parent` 는 **물품 축을 자기 자신으로** 가리킨다(팔레트에 담긴 상자 — EPCIS `AggregationEvent`).
|
|
194
|
+
* `carriedBy` 는 다른 축이다 — 반복사용 자산(GRAI)이 물류단위를 실어 나른다(§`FlowItem.carriedBy`).
|
|
195
|
+
*
|
|
196
|
+
* 관계 이름은 **소문자 한 낱말**이다(`twin.rel.<name>`) — 기존 열여덟 개가 그 규율이고 시험이 지킨다.
|
|
197
|
+
*
|
|
198
|
+
* 품목(`gtin`)은 오더와 **같은 규율**이다: 자재 키가 아니라 GS1 품목 참조이므로 축을 직접 가리키지
|
|
199
|
+
* 않고 `external` 로 둔다. 축을 가리키게 적으면 없는 필드를 가리키는 선언이 된다.
|
|
200
|
+
*/
|
|
201
|
+
{ from: 'items', field: 'location', target: { kind: 'axis', axis: 'locations' }, via: 'twin.rel.at' },
|
|
202
|
+
{ from: 'items', field: 'parent', target: { kind: 'axis', axis: 'items' }, via: 'twin.rel.parent', optional: true },
|
|
203
|
+
{ from: 'items', field: 'carriedBy', target: { kind: 'axis', axis: 'assets' }, via: 'twin.rel.asset', optional: true },
|
|
204
|
+
{ from: 'items', field: 'gtin', target: { kind: 'external', entity: 'gs1.itemRef' }, via: 'twin.rel.item', optional: true },
|
|
166
205
|
/*
|
|
167
206
|
* 자격을 검증한 시험 — **여덟 갈래.** 자원(개체)과 등급 양쪽이 가리킨다: 표준이 그 둘 모두에 이
|
|
168
207
|
* 참조를 두었기 때문이다(개체는 "이 사람이 통과했다", 등급은 "이 자격은 이 시험을 요구한다").
|
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
|
@@ -388,7 +388,20 @@ export class ItemStore {
|
|
|
388
388
|
clone() {
|
|
389
389
|
const out = new ItemStore();
|
|
390
390
|
for (const [k, it] of this.map)
|
|
391
|
-
out.set(k, structuredClone(it));
|
|
391
|
+
out.map.set(k, structuredClone(it));
|
|
392
|
+
/*
|
|
393
|
+
* ── 색인은 **베껴 온다** (2026-08-22) ─────────────────────────────────────
|
|
394
|
+
* 예전에는 항목마다 `set()` 을 불러 색인을 다시 쌓았다. 색인이 하나일 때는 그 비용이 묻혔는데,
|
|
395
|
+
* 품목 색인이 생기면서 항목마다 Map 조회 둘 + Set 삽입 둘이 됐다 — 그리고 `fork()` 가 이 함수를
|
|
396
|
+
* 쓴다. 예측은 회차마다 사본을 뜨므로 그 비용이 곧바로 예측 비용이다.
|
|
397
|
+
*
|
|
398
|
+
* 사본은 **원본과 같은 배치**이므로 색인을 다시 계산할 이유가 없다. 그룹마다 Set 하나를 만들어
|
|
399
|
+
* 베낀다. 어긋날 위험은 `indexDrift()` 가 지킨다(시나리오를 돌린 뒤 그 값으로 확인한다).
|
|
400
|
+
*/
|
|
401
|
+
for (const [loc, keys] of this.byLocation)
|
|
402
|
+
out.byLocation.set(loc, new Set(keys));
|
|
403
|
+
for (const [g, keys] of this.byGtin)
|
|
404
|
+
out.byGtin.set(g, new Set(keys));
|
|
392
405
|
return out;
|
|
393
406
|
}
|
|
394
407
|
/*
|
|
@@ -819,7 +832,26 @@ export class FlowEngine {
|
|
|
819
832
|
this.seedDanglingRefs += dropped;
|
|
820
833
|
return { kept, dropped };
|
|
821
834
|
}
|
|
822
|
-
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') {
|
|
823
855
|
/* 확인 처리를 먼저 이어받는다 — 아래에서 상태를 주입하면 곧바로 주목 신호가 계산되므로, 늦게
|
|
824
856
|
* 이어받으면 그 한 번은 확인 안 된 것으로 계산된다(화면이 잠깐 빨개진다). */
|
|
825
857
|
for (const id of snap.acked ?? [])
|
|
@@ -869,7 +901,15 @@ export class FlowEngine {
|
|
|
869
901
|
const prev = this.equipment.get(m.id);
|
|
870
902
|
this.equipment.set(m.id, {
|
|
871
903
|
...(prev ?? {}),
|
|
872
|
-
|
|
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,
|
|
873
913
|
runMs: oee?.runMs ?? 0, setupMs: oee?.setupMs ?? 0, downMs: oee?.downMs ?? 0,
|
|
874
914
|
goodCount: oee?.goodCount ?? 0, scrapCount: oee?.scrapCount ?? 0,
|
|
875
915
|
/*
|
|
@@ -902,7 +942,10 @@ export class FlowEngine {
|
|
|
902
942
|
const prev = this.persons.get(p.id);
|
|
903
943
|
this.persons.set(p.id, {
|
|
904
944
|
...(prev ?? {}),
|
|
905
|
-
|
|
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,
|
|
906
949
|
...(p.location ? { location: p.location } : {}),
|
|
907
950
|
...(p.properties ? { properties: p.properties } : {}),
|
|
908
951
|
...(p.testSpecificationIds ? { testSpecificationIds: p.testSpecificationIds } : {}),
|
|
@@ -916,7 +959,10 @@ export class FlowEngine {
|
|
|
916
959
|
const prev = this.assets.get(a.id);
|
|
917
960
|
this.assets.set(a.id, {
|
|
918
961
|
...(prev ?? {}),
|
|
919
|
-
|
|
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,
|
|
920
966
|
...(a.properties ? { properties: a.properties } : {}),
|
|
921
967
|
...(a.testSpecificationIds ? { testSpecificationIds: a.testSpecificationIds } : {}),
|
|
922
968
|
/* 결과도 이어받는다 — 잃으면 예측이 **자격 만료를 모르는 현장**에서 출발한다(낙관 쪽으로 치우친다). */
|
|
@@ -1009,38 +1055,83 @@ export class FlowEngine {
|
|
|
1009
1055
|
}
|
|
1010
1056
|
/* 작업이 딛고 설 오더를 먼저 세운다 — 순서가 뒤바뀌면 아래 확인이 언제나 "없다" 로 답한다. */
|
|
1011
1057
|
const seededOrderIds = new Set(this.orders.keys());
|
|
1012
|
-
|
|
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';
|
|
1013
1073
|
for (const t of snap.tasks ?? []) {
|
|
1014
|
-
if (t.status === 'completed')
|
|
1074
|
+
if (filtering && t.status === 'completed') {
|
|
1075
|
+
droppedCompleted++;
|
|
1015
1076
|
continue;
|
|
1077
|
+
}
|
|
1016
1078
|
const ref = t.itemRefs?.[0];
|
|
1017
|
-
if (ref && !this.itemByRef(ref)) {
|
|
1018
|
-
|
|
1079
|
+
if (filtering && ref && !this.itemByRef(ref)) {
|
|
1080
|
+
droppedNoItem++;
|
|
1019
1081
|
continue;
|
|
1020
1082
|
}
|
|
1021
1083
|
/* 오더도 같다 — 이미 이행된 오더는 주입하지 않으므로(위 `remaining <= 0`), 그 오더에 딸린 작업만
|
|
1022
1084
|
남으면 완료 시점에 없는 오더를 딛는다. 주입 단계에서 함께 뺀다. */
|
|
1023
|
-
if (t.orderId && !seededOrderIds.has(t.orderId)) {
|
|
1024
|
-
|
|
1085
|
+
if (filtering && t.orderId && !seededOrderIds.has(t.orderId)) {
|
|
1086
|
+
droppedNoOrder++;
|
|
1025
1087
|
continue;
|
|
1026
1088
|
}
|
|
1027
1089
|
const known = typeof t.remainingMs === 'number' && Number.isFinite(t.remainingMs);
|
|
1028
1090
|
this.tasks.set(t.id, {
|
|
1029
1091
|
id: t.id, kind: t.kind,
|
|
1030
|
-
|
|
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',
|
|
1031
1101
|
/* 이미 일어난 자재 이동은 **씨앗에도 남는다** — 잃으면 실적이 재기동마다 지워진다. */
|
|
1032
1102
|
...(t.materialActual?.length ? { materialActual: t.materialActual.map(r => ({ ...r })) } : {}),
|
|
1033
1103
|
itemEpc: t.itemRefs?.[0] ?? '',
|
|
1034
1104
|
fromNode: t.fromNode ?? '', toNode: t.toNode ?? '',
|
|
1035
|
-
|
|
1105
|
+
/*
|
|
1106
|
+
* 자원 참조는 **작업 자신의 사실**이다. 씨앗에서는 굴릴 수 있는 작업만 자원을 잡게 하지만
|
|
1107
|
+
* (남은 시간을 모르면 배정 루프가 다시 잡아야 한다), 관측에서는 원본이 말한 참조를 지운 이유가
|
|
1108
|
+
* 없다 — 지우면 「어느 설비가 이 일을 하고 있나」가 화면에서 사라진다.
|
|
1109
|
+
*/
|
|
1110
|
+
resource: purpose === 'observe' ? (t.resourceRef ?? null) : known && t.status === 'in-progress' ? (t.resourceRef ?? null) : null,
|
|
1036
1111
|
remainingMs: known ? t.remainingMs : (t.durationMs ?? 0),
|
|
1037
1112
|
startedAtSimMs: t.startedAtSimMs,
|
|
1038
1113
|
durationMs: t.durationMs ?? (known ? t.remainingMs : 0),
|
|
1039
1114
|
orderId: t.orderId,
|
|
1040
1115
|
intent: t.intent
|
|
1041
1116
|
});
|
|
1042
|
-
/*
|
|
1043
|
-
|
|
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) {
|
|
1044
1135
|
const mv = this.equipment.get(t.resourceRef);
|
|
1045
1136
|
if (mv) {
|
|
1046
1137
|
mv.status = 'busy';
|
|
@@ -1048,7 +1139,7 @@ export class FlowEngine {
|
|
|
1048
1139
|
}
|
|
1049
1140
|
}
|
|
1050
1141
|
/* 사람도 같다 — 진행 중이던 작업에 투입돼 있던 사람은 여전히 점유되어 있어야 한다. */
|
|
1051
|
-
if (known && t.status === 'in-progress') {
|
|
1142
|
+
if (purpose === 'seed' && known && t.status === 'in-progress') {
|
|
1052
1143
|
const restored = this.tasks.get(t.id);
|
|
1053
1144
|
if (restored)
|
|
1054
1145
|
restored.personnel = t.personnel ? [...t.personnel] : undefined;
|
|
@@ -1061,9 +1152,21 @@ export class FlowEngine {
|
|
|
1061
1152
|
}
|
|
1062
1153
|
}
|
|
1063
1154
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
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").)');
|
|
1067
1170
|
}
|
|
1068
1171
|
}
|
|
1069
1172
|
/**
|
|
@@ -1710,7 +1813,14 @@ export class FlowEngine {
|
|
|
1710
1813
|
if (!this.observedDirty || !this.observer)
|
|
1711
1814
|
return;
|
|
1712
1815
|
this.observedDirty = false;
|
|
1713
|
-
|
|
1816
|
+
/*
|
|
1817
|
+
* **목적을 밝힌다 — 이것은 씨앗이 아니라 관측 정착이다**(§`hydrateObserved` 의 `purpose`).
|
|
1818
|
+
*
|
|
1819
|
+
* 이 한 낱말이 없던 동안 미러의 작업이 씨앗 규칙에 걸려 사라졌다: 포천 미러의 저널에
|
|
1820
|
+
* `task.status` 5,786건이 있는데 `getSnapshot().tasks` 는 0 이었고, 화면은 「아직 하나도
|
|
1821
|
+
* 없습니다」라고 말했다. 미러는 그 작업을 굴리지 않는다 — 들은 것을 말할 뿐이다.
|
|
1822
|
+
*/
|
|
1823
|
+
this.hydrateObserved(this.observer.snapshot(), [], 'observe');
|
|
1714
1824
|
}
|
|
1715
1825
|
/** 구독자 목록 — 관측 재방출용(private handlers 에 접근). */
|
|
1716
1826
|
observedHandlers() {
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -2549,6 +2549,34 @@ var TWIN_AXES = [
|
|
|
2549
2549
|
standardClass: { isa95: "SegmentResponse", epcis: "TransformationEvent" },
|
|
2550
2550
|
systems: LOGISTICS
|
|
2551
2551
|
},
|
|
2552
|
+
/*
|
|
2553
|
+
* ── 물품이 축이 아니었다 (2026-08-22) ──────────────────────────────────────
|
|
2554
|
+
* 트윈에서 **수가 가장 많은 것**이 물품인데(실측: 엔티티 3,611 중 2,400 · hatio-us 는 2,805) 그것을
|
|
2555
|
+
* 가리켜 걸어 들어갈 자리가 없었다. 지도와 집약 태그에는 이미 보이는데 「모델 살펴보기」에는 문이
|
|
2556
|
+
* 없었다 — 사용자가 트윈에 가장 자주 묻는 것이 「내 물건이 어디 있나」이므로 그것은 접근 장벽이다.
|
|
2557
|
+
*
|
|
2558
|
+
* 성격은 `orders`·`tasks` 와 같다: 상태에 살고, 저널에 이력이 있고, 원본이 낸 것을 트윈이 관측한다.
|
|
2559
|
+
* 그래서 같은 조합(`instance` · `state` · `historical`)이다.
|
|
2560
|
+
*
|
|
2561
|
+
* ── 표준 대응 ─────────────────────────────────────────────────────────────
|
|
2562
|
+
* ISA-95 는 `MaterialLot` 이다 — 물품은 「무슨 품목인가」(정의)가 아니라 「그 품목의 이 덩어리」이고,
|
|
2563
|
+
* 위치·수량·부분(`MaterialSubLot`)을 그 자리가 든다. EPCIS 는 `ObjectEvent` 다: 개체가 생기고
|
|
2564
|
+
* 관측되고 사라지는 것을 그 사건이 말한다.
|
|
2565
|
+
*
|
|
2566
|
+
* ── 무엇이 이 항목을 가리키나 ─────────────────────────────────────────────
|
|
2567
|
+
* `subLotId ?? epc` 다. 짐작에 맡기면 `gtin` 으로 떨어지고, 그러면 **같은 품목의 물품 전부가 한
|
|
2568
|
+
* 식별자로 뭉친다** — 화면이 2,400개를 몇 개로 보인다.
|
|
2569
|
+
*/
|
|
2570
|
+
{
|
|
2571
|
+
axis: "items",
|
|
2572
|
+
label: "twin.axis.items",
|
|
2573
|
+
kind: "instance",
|
|
2574
|
+
source: "state",
|
|
2575
|
+
historical: true,
|
|
2576
|
+
idField: ["subLotId", "epc"],
|
|
2577
|
+
standardClass: { isa95: "MaterialLot", epcis: "ObjectEvent" },
|
|
2578
|
+
systems: LOGISTICS
|
|
2579
|
+
},
|
|
2552
2580
|
/*
|
|
2553
2581
|
* ── 에너지가 더하는 개념은 **하나**다 (2026-08-14, §10 6.5단계) ──────────────
|
|
2554
2582
|
*
|
|
@@ -2623,6 +2651,24 @@ var TWIN_RELATIONS = [
|
|
|
2623
2651
|
{ from: "tasks", field: "toNode", target: { kind: "axis", axis: "locations" }, via: "twin.rel.at", optional: true },
|
|
2624
2652
|
{ from: "tasks", field: "resourceRef", target: { kind: "axis", axis: "equipment" }, via: "twin.rel.by", optional: true },
|
|
2625
2653
|
{ from: "tasks", field: "personnel[]", target: { kind: "axis", axis: "persons" }, via: "twin.rel.crew", optional: true },
|
|
2654
|
+
/*
|
|
2655
|
+
* 물품의 관계 (2026-08-22) — 없으면 축이 **걸어 들어갈 수 없는 목록**이 된다.
|
|
2656
|
+
*
|
|
2657
|
+
* `location` 은 필수다 — 물품은 언제나 어딘가에 있다(그것이 물품의 뜻이다). 나머지는 선택이다:
|
|
2658
|
+
* 물류단위에 담기지 않은 물품, 자산에 실리지 않은 팔레트가 정상이다.
|
|
2659
|
+
*
|
|
2660
|
+
* `parent` 는 **물품 축을 자기 자신으로** 가리킨다(팔레트에 담긴 상자 — EPCIS `AggregationEvent`).
|
|
2661
|
+
* `carriedBy` 는 다른 축이다 — 반복사용 자산(GRAI)이 물류단위를 실어 나른다(§`FlowItem.carriedBy`).
|
|
2662
|
+
*
|
|
2663
|
+
* 관계 이름은 **소문자 한 낱말**이다(`twin.rel.<name>`) — 기존 열여덟 개가 그 규율이고 시험이 지킨다.
|
|
2664
|
+
*
|
|
2665
|
+
* 품목(`gtin`)은 오더와 **같은 규율**이다: 자재 키가 아니라 GS1 품목 참조이므로 축을 직접 가리키지
|
|
2666
|
+
* 않고 `external` 로 둔다. 축을 가리키게 적으면 없는 필드를 가리키는 선언이 된다.
|
|
2667
|
+
*/
|
|
2668
|
+
{ from: "items", field: "location", target: { kind: "axis", axis: "locations" }, via: "twin.rel.at" },
|
|
2669
|
+
{ from: "items", field: "parent", target: { kind: "axis", axis: "items" }, via: "twin.rel.parent", optional: true },
|
|
2670
|
+
{ from: "items", field: "carriedBy", target: { kind: "axis", axis: "assets" }, via: "twin.rel.asset", optional: true },
|
|
2671
|
+
{ from: "items", field: "gtin", target: { kind: "external", entity: "gs1.itemRef" }, via: "twin.rel.item", optional: true },
|
|
2626
2672
|
/*
|
|
2627
2673
|
* 자격을 검증한 시험 — **여덟 갈래.** 자원(개체)과 등급 양쪽이 가리킨다: 표준이 그 둘 모두에 이
|
|
2628
2674
|
* 참조를 두었기 때문이다(개체는 "이 사람이 통과했다", 등급은 "이 자격은 이 시험을 요구한다").
|
|
@@ -3515,7 +3561,9 @@ var ItemStore = class _ItemStore {
|
|
|
3515
3561
|
*/
|
|
3516
3562
|
clone() {
|
|
3517
3563
|
const out = new _ItemStore();
|
|
3518
|
-
for (const [k, it] of this.map) out.set(k, structuredClone(it));
|
|
3564
|
+
for (const [k, it] of this.map) out.map.set(k, structuredClone(it));
|
|
3565
|
+
for (const [loc, keys] of this.byLocation) out.byLocation.set(loc, new Set(keys));
|
|
3566
|
+
for (const [g, keys] of this.byGtin) out.byGtin.set(g, new Set(keys));
|
|
3519
3567
|
return out;
|
|
3520
3568
|
}
|
|
3521
3569
|
/*
|
|
@@ -3916,7 +3964,7 @@ var FlowEngine = class {
|
|
|
3916
3964
|
this.seedDanglingRefs += dropped;
|
|
3917
3965
|
return { kept, dropped };
|
|
3918
3966
|
}
|
|
3919
|
-
hydrateObserved(snap, orders = []) {
|
|
3967
|
+
hydrateObserved(snap, orders = [], purpose = "seed") {
|
|
3920
3968
|
for (const id of snap.acked ?? []) this._acked.add(id);
|
|
3921
3969
|
for (const e of snap.attentionSince ?? []) if (e?.id && e.since) this._attentionSince.set(e.id, e.since);
|
|
3922
3970
|
for (const n of snap.locations) {
|
|
@@ -3946,12 +3994,20 @@ var FlowEngine = class {
|
|
|
3946
3994
|
const prev = this.equipment.get(m.id);
|
|
3947
3995
|
this.equipment.set(m.id, {
|
|
3948
3996
|
...prev ?? {},
|
|
3997
|
+
/*
|
|
3998
|
+
* `taskId` — **관측이면 원본이 말한 값이다.**
|
|
3999
|
+
*
|
|
4000
|
+
* 씨앗에서는 `null` 로 두고 작업 복원이 다시 세운다(작업이 함께 오지 않으면 그 자원이 영원히
|
|
4001
|
+
* 잡혀 있게 되므로). 그런데 관측에서는 `equipment.status` 가 이 값을 **직접 실어 온다** —
|
|
4002
|
+
* 상태(busy)는 그 축에서 받고 묶임은 작업에서 되세우면, 같은 사실의 두 조각이 서로 다른 축에서
|
|
4003
|
+
* 오게 되고 한쪽만 도착한 순간 어긋난다.
|
|
4004
|
+
*/
|
|
3949
4005
|
id: m.id,
|
|
3950
4006
|
kind: m.kind,
|
|
3951
4007
|
location: m.location ?? "",
|
|
3952
4008
|
...m.homeLocation ? { homeLocation: m.homeLocation } : {},
|
|
3953
4009
|
status: m.status ?? "idle",
|
|
3954
|
-
taskId: null,
|
|
4010
|
+
taskId: purpose === "observe" ? m.taskId ?? null : null,
|
|
3955
4011
|
runMs: oee?.runMs ?? 0,
|
|
3956
4012
|
setupMs: oee?.setupMs ?? 0,
|
|
3957
4013
|
downMs: oee?.downMs ?? 0,
|
|
@@ -3982,10 +4038,11 @@ var FlowEngine = class {
|
|
|
3982
4038
|
const prev = this.persons.get(p.id);
|
|
3983
4039
|
this.persons.set(p.id, {
|
|
3984
4040
|
...prev ?? {},
|
|
4041
|
+
/* 설비와 같은 규율 — 관측이면 원본이 말한 상태·묶임을 지킨다(씨앗이면 작업 복원이 세운다). */
|
|
3985
4042
|
id: p.id,
|
|
3986
4043
|
personnelClassIds: p.personnelClassIds ?? prev?.personnelClassIds,
|
|
3987
|
-
status: "idle",
|
|
3988
|
-
taskId: null,
|
|
4044
|
+
status: purpose === "observe" ? p.status ?? "idle" : "idle",
|
|
4045
|
+
taskId: purpose === "observe" ? p.taskId ?? null : null,
|
|
3989
4046
|
...p.location ? { location: p.location } : {},
|
|
3990
4047
|
...p.properties ? { properties: p.properties } : {},
|
|
3991
4048
|
...p.testSpecificationIds ? { testSpecificationIds: p.testSpecificationIds } : {},
|
|
@@ -3998,11 +4055,12 @@ var FlowEngine = class {
|
|
|
3998
4055
|
const prev = this.assets.get(a.id);
|
|
3999
4056
|
this.assets.set(a.id, {
|
|
4000
4057
|
...prev ?? {},
|
|
4058
|
+
/* 설비·사람과 같은 규율 — 관측이면 원본이 말한 상태·묶임을 지킨다. */
|
|
4001
4059
|
id: a.id,
|
|
4002
4060
|
assetClassIds: a.assetClassIds ?? prev?.assetClassIds,
|
|
4003
4061
|
location: a.location,
|
|
4004
|
-
status: "idle",
|
|
4005
|
-
taskId: null,
|
|
4062
|
+
status: purpose === "observe" ? a.status ?? "idle" : "idle",
|
|
4063
|
+
taskId: purpose === "observe" ? a.taskId ?? null : null,
|
|
4006
4064
|
carrying: a.carrying,
|
|
4007
4065
|
...a.properties ? { properties: a.properties } : {},
|
|
4008
4066
|
...a.testSpecificationIds ? { testSpecificationIds: a.testSpecificationIds } : {},
|
|
@@ -4071,43 +4129,66 @@ var FlowEngine = class {
|
|
|
4071
4129
|
});
|
|
4072
4130
|
}
|
|
4073
4131
|
const seededOrderIds = new Set(this.orders.keys());
|
|
4074
|
-
let
|
|
4132
|
+
let droppedNoItem = 0;
|
|
4133
|
+
let droppedNoOrder = 0;
|
|
4134
|
+
let droppedCompleted = 0;
|
|
4135
|
+
const filtering = purpose === "seed";
|
|
4075
4136
|
for (const t of snap.tasks ?? []) {
|
|
4076
|
-
if (t.status === "completed")
|
|
4137
|
+
if (filtering && t.status === "completed") {
|
|
4138
|
+
droppedCompleted++;
|
|
4139
|
+
continue;
|
|
4140
|
+
}
|
|
4077
4141
|
const ref = t.itemRefs?.[0];
|
|
4078
|
-
if (ref && !this.itemByRef(ref)) {
|
|
4079
|
-
|
|
4142
|
+
if (filtering && ref && !this.itemByRef(ref)) {
|
|
4143
|
+
droppedNoItem++;
|
|
4080
4144
|
continue;
|
|
4081
4145
|
}
|
|
4082
|
-
if (t.orderId && !seededOrderIds.has(t.orderId)) {
|
|
4083
|
-
|
|
4146
|
+
if (filtering && t.orderId && !seededOrderIds.has(t.orderId)) {
|
|
4147
|
+
droppedNoOrder++;
|
|
4084
4148
|
continue;
|
|
4085
4149
|
}
|
|
4086
4150
|
const known = typeof t.remainingMs === "number" && Number.isFinite(t.remainingMs);
|
|
4087
4151
|
this.tasks.set(t.id, {
|
|
4088
4152
|
id: t.id,
|
|
4089
4153
|
kind: t.kind,
|
|
4090
|
-
|
|
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",
|
|
4091
4163
|
/* 이미 일어난 자재 이동은 **씨앗에도 남는다** — 잃으면 실적이 재기동마다 지워진다. */
|
|
4092
4164
|
...t.materialActual?.length ? { materialActual: t.materialActual.map((r) => ({ ...r })) } : {},
|
|
4093
4165
|
itemEpc: t.itemRefs?.[0] ?? "",
|
|
4094
4166
|
fromNode: t.fromNode ?? "",
|
|
4095
4167
|
toNode: t.toNode ?? "",
|
|
4096
|
-
|
|
4168
|
+
/*
|
|
4169
|
+
* 자원 참조는 **작업 자신의 사실**이다. 씨앗에서는 굴릴 수 있는 작업만 자원을 잡게 하지만
|
|
4170
|
+
* (남은 시간을 모르면 배정 루프가 다시 잡아야 한다), 관측에서는 원본이 말한 참조를 지운 이유가
|
|
4171
|
+
* 없다 — 지우면 「어느 설비가 이 일을 하고 있나」가 화면에서 사라진다.
|
|
4172
|
+
*/
|
|
4173
|
+
resource: purpose === "observe" ? t.resourceRef ?? null : known && t.status === "in-progress" ? t.resourceRef ?? null : null,
|
|
4097
4174
|
remainingMs: known ? t.remainingMs : t.durationMs ?? 0,
|
|
4098
4175
|
startedAtSimMs: t.startedAtSimMs,
|
|
4099
4176
|
durationMs: t.durationMs ?? (known ? t.remainingMs : 0),
|
|
4100
4177
|
orderId: t.orderId,
|
|
4101
4178
|
intent: t.intent
|
|
4102
4179
|
});
|
|
4103
|
-
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) {
|
|
4104
4185
|
const mv = this.equipment.get(t.resourceRef);
|
|
4105
4186
|
if (mv) {
|
|
4106
4187
|
mv.status = "busy";
|
|
4107
4188
|
mv.taskId = t.id;
|
|
4108
4189
|
}
|
|
4109
4190
|
}
|
|
4110
|
-
if (known && t.status === "in-progress") {
|
|
4191
|
+
if (purpose === "seed" && known && t.status === "in-progress") {
|
|
4111
4192
|
const restored = this.tasks.get(t.id);
|
|
4112
4193
|
if (restored) restored.personnel = t.personnel ? [...t.personnel] : void 0;
|
|
4113
4194
|
for (const id of t.personnel ?? []) {
|
|
@@ -4119,9 +4200,15 @@ var FlowEngine = class {
|
|
|
4119
4200
|
}
|
|
4120
4201
|
}
|
|
4121
4202
|
}
|
|
4122
|
-
|
|
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 ");
|
|
4123
4210
|
console.warn(
|
|
4124
|
-
`[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").)`
|
|
4125
4212
|
);
|
|
4126
4213
|
}
|
|
4127
4214
|
}
|
|
@@ -4704,7 +4791,7 @@ var FlowEngine = class {
|
|
|
4704
4791
|
settleObserved() {
|
|
4705
4792
|
if (!this.observedDirty || !this.observer) return;
|
|
4706
4793
|
this.observedDirty = false;
|
|
4707
|
-
this.hydrateObserved(this.observer.snapshot());
|
|
4794
|
+
this.hydrateObserved(this.observer.snapshot(), [], "observe");
|
|
4708
4795
|
}
|
|
4709
4796
|
/** 구독자 목록 — 관측 재방출용(private handlers 에 접근). */
|
|
4710
4797
|
observedHandlers() {
|
package/package.json
CHANGED