@operato/twin-kernel 0.11.27 → 0.11.29

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.
@@ -825,6 +825,11 @@ export declare abstract class FlowEngine implements TwinKernel {
825
825
  id: string;
826
826
  since: string;
827
827
  }[];
828
+ /**
829
+ * **접힌 오더**(ADR-0092) — 저널이나 원천을 다시 읽어 세우는 트윈도 이어받아야 한다. 이것이 없으면 세는 수를 잃고,
830
+ * 원천이 옛 종결을 다시 보낼 때 그것을 거를 경계가 없어 접힌 오더가 뜨거운 줄로 되살아난다.
831
+ */
832
+ foldedOrders?: FoldedOrders;
828
833
  }): void;
829
834
  /**
830
835
  * 씨앗의 확보분 중 **이 트윈에 실제로 있는 것만** 남긴다 — 없는 것은 세고 버린다.
@@ -1049,6 +1049,21 @@ export class FlowEngine {
1049
1049
  for (const e of snap?.attentionSince ?? [])
1050
1050
  if (e?.id && e.since)
1051
1051
  this._attentionSince.set(e.id, e.since);
1052
+ if (snap?.foldedOrders && foldedOrderCount(snap.foldedOrders)) {
1053
+ /*
1054
+ * 이미 접힌 것이 있으면(재개점이 먼저 되세웠다) 건드리지 않는다 — 같은 값을 또 더하면 세는 수가 두 번 는다.
1055
+ * 미러는 리듀서가 원천이다 — 거기에 넣어야 다시 온 옛 종결을 거른다. 리듀서가 아직 없으면 만든다(§restoreObserved).
1056
+ */
1057
+ if (this.observeMode) {
1058
+ if (!this.observer)
1059
+ this.observer = new ObservedReducer(this.boardDef ?? { locations: [], equipment: [] });
1060
+ if (this.observer.adoptFoldedOrders(snap.foldedOrders))
1061
+ this.observedDirty = true;
1062
+ }
1063
+ else if (!foldedOrderCount(this.foldedOrders)) {
1064
+ this.foldedOrders = structuredClone(snap.foldedOrders);
1065
+ }
1066
+ }
1052
1067
  }
1053
1068
  /**
1054
1069
  * 씨앗의 확보분 중 **이 트윈에 실제로 있는 것만** 남긴다 — 없는 것은 세고 버린다.
@@ -1887,6 +1902,8 @@ export class FlowEngine {
1887
1902
  })),
1888
1903
  /* 접힌 오더 — 접은 것이 없으면 싣지 않는다(ADR-0092). */
1889
1904
  ...(foldedOrderCount(this.foldedOrders) ? { foldedOrders: structuredClone(this.foldedOrders) } : {}),
1905
+ /* 다른 렌즈의 자리 — 모델의 선언 그대로(ADR-0081 보탬). 시뮬과 미러가 같은 칸을 낸다. */
1906
+ ...(this.boardDef?.referencedLocations?.length ? { referencedLocations: this.boardDef.referencedLocations.map(r => ({ ...r })) } : {}),
1890
1907
  orders: [...this.orders.values()].map(o => ({
1891
1908
  id: o.id, kind: o.kind, status: o.status,
1892
1909
  ...(o.terminalAtMs !== undefined ? { terminalAtMs: o.terminalAtMs } : {}),
@@ -1,4 +1,5 @@
1
1
  import type { AssetState, TestResult, MaterialQuantity, TwinModelDef, CanonicalEnvelope, LocationState, ItemState, LocationObservation, EquipmentState, PersonState, TaskState, OrderState, StructureShift, DispositionFact } from '@operato/ops-contract';
2
+ import { type ReferencedLocation } from '@operato/ops-contract';
2
3
  import { type FoldedOrders } from '@operato/ops-contract';
3
4
  import type { MaterialLotUse } from '@operato/ops-contract';
4
5
  import { type VocabularyElement } from '@operato/ops-contract';
@@ -96,6 +97,8 @@ export interface ProjectedState {
96
97
  lastAtMs?: number;
97
98
  }[];
98
99
  locations: LocationState[];
100
+ /** 다른 렌즈의 자리 — 참조만 한다(§`ReferencedLocation`). 선언이 없으면 칸이 없다. */
101
+ referencedLocations?: ReferencedLocation[];
99
102
  items: ItemState[];
100
103
  /** 사람 — 등급·교대·투입. 인원을 선언하지 않은 트윈에서는 빈 배열. */
101
104
  persons: PersonState[];
@@ -306,6 +309,8 @@ export declare class ObservedReducer {
306
309
  private classDefs;
307
310
  /** 시각 해석 기준(트윈 모델 선언) — 없으면 UTC. 캘린더의 `HH:MM` 이 어느 기준인지 정한다. */
308
311
  private utcOffsetMinutes?;
312
+ private referencedDecl;
313
+ private referenced;
309
314
  constructor(model: TwinModelDef);
310
315
  /**
311
316
  * **구조를 전환한다** — 관측된 사실은 지키고 토폴로지만 새 선언으로 바꾼다.
@@ -525,6 +530,13 @@ export declare class ObservedReducer {
525
530
  * 모델에서 오는 판정 입력(교대·등급·시간대)은 생성자가 이미 세웠으므로 건드리지 않는다.
526
531
  */
527
532
  restore(cp: ReducerCheckpoint): void;
533
+ /**
534
+ * **이어받은 접힌 쪽을 앉힌다**(ADR-0092) — 이어가기 씨앗으로 뜨는 미러가 부른다(§`FlowEngine.hydrateContinuity`).
535
+ *
536
+ * 이미 접힌 것이 있으면 **건드리지 않는다.** 재개점(체크포인트)이 먼저 되세웠다면 그것이 정본이고, 같은 값을 또 더하면 세는
537
+ * 수가 두 번 는다. 비어 있을 때만 이어받는다. 이어받았으면 참을 돌려준다.
538
+ */
539
+ adoptFoldedOrders(folded: FoldedOrders): boolean;
528
540
  /**
529
541
  * **끝난 오더를 접는다**(ADR-0092) — 접은 수를 돌려준다.
530
542
  *
@@ -21,7 +21,7 @@
21
21
  */
22
22
  import { computeOee, isWorkShiftExecutionBasis } from '@operato/ops-contract';
23
23
  import { OP_EVENT, capabilityOf, itemKeyOf, judgeAgainstSpec, requiredTestsFor, locationStatusOf, readBoardEquipment, readBoardLocations, readBoardAssets, effectivityAt, offCalendarAt, offCalendarReasonAt, activeShiftAt } from '@operato/ops-contract';
24
- import { ILMD_ATTR, parseEpc } from '@operato/ops-contract';
24
+ import { ILMD_ATTR, parseEpc, referencedLocationIds } from '@operato/ops-contract';
25
25
  import { emptyFoldedOrders, foldOrdersInto, foldedOrderCount, pruneFoldedHours, isOrderTerminal, orderFoldPolicyOf, planOrderFold, standsNewOrderRow } from '@operato/ops-contract';
26
26
  import { expiryFromAttributes, lotFromAttributes } from '@operato/ops-contract';
27
27
  /* 클래스 마스터 표는 상태를 들므로 커널에 있다(§`class-master-table`). */
@@ -125,6 +125,12 @@ export class ObservedReducer {
125
125
  classDefs = {};
126
126
  /** 시각 해석 기준(트윈 모델 선언) — 없으면 UTC. 캘린더의 `HH:MM` 이 어느 기준인지 정한다. */
127
127
  utcOffsetMinutes;
128
+ /*
129
+ * **다른 렌즈의 자리** — 참조만 한다(ADR-0081 보탬). 관측으로 자리로 올리지 않고, 거기로 간 물품은 이 렌즈의 재고에서 뺀다.
130
+ * 창고가 조립 라인으로 불출한 자재는 창고 재고가 아니다 — 그 자리를 「종류 모르는 자리」로 세우면 재고가 거기 남는다.
131
+ */
132
+ referencedDecl = [];
133
+ referenced = new Set();
128
134
  constructor(model) {
129
135
  /* 접지 않는 선언(0 · 무한)은 거절한다 — 기본값으로 조용히 바꾸면 선언한 사람은 자기 값이 쓰이는 줄 안다. */
130
136
  const fold = orderFoldPolicyOf(model.orderFold);
@@ -136,6 +142,8 @@ export class ObservedReducer {
136
142
  /* 선언된 판정 기준 — 원천이 판정하지 않은 결과를 커널이 판정할 때 쓴다(§`judgeAgainstSpec`). */
137
143
  for (const sp of model.testSpecifications ?? [])
138
144
  this.testSpecs.set(sp.id, sp);
145
+ this.referencedDecl = [...(model.referencedLocations ?? [])];
146
+ this.referenced = referencedLocationIds(model);
139
147
  for (const n of readBoardLocations(model))
140
148
  this.master.set(n.id, { id: n.id, type: n.type, capacity: n.capacity, parallelism: n.parallelism, parentId: n.parentId, origin: 'master' });
141
149
  // 설비 기준선(마스터) — equipment.status 델타로 갱신됨.
@@ -177,6 +185,17 @@ export class ObservedReducer {
177
185
  const declaredLocationIds = new Set(locations.map(n => n.id));
178
186
  let locationsAdded = 0;
179
187
  let locationsDropped = 0;
188
+ this.referencedDecl = [...(model.referencedLocations ?? [])];
189
+ this.referenced = referencedLocationIds(model);
190
+ /* 새로 참조가 된 자리 — 관측으로 올라 있던 줄은 내리고, 거기 앉아 있던 물품은 이 렌즈의 재고에서 뺀다(참조의 뜻 그대로). */
191
+ for (const [id, cur] of [...this.master]) {
192
+ if (cur.origin === 'observed' && this.referenced.has(id))
193
+ this.master.delete(id);
194
+ }
195
+ for (const [key, it] of [...this.items]) {
196
+ if (it.location && this.referenced.has(it.location))
197
+ this.remove(key);
198
+ }
180
199
  for (const [id, cur] of [...this.master]) {
181
200
  if (declaredLocationIds.has(id) || cur.origin === 'observed')
182
201
  continue;
@@ -256,7 +275,7 @@ export class ObservedReducer {
256
275
  * (트윈 모델에 좌표가 없고, 용량을 채워야 계획에 참여한다). 마스터 동기가 오면 `master` 로 승격된다.
257
276
  */
258
277
  touchLocation(id) {
259
- if (!id || this.master.has(id))
278
+ if (!id || this.master.has(id) || this.referenced.has(id))
260
279
  return;
261
280
  this.master.set(id, { id, type: UNKNOWN_TYPE, origin: 'observed' });
262
281
  }
@@ -935,6 +954,9 @@ export class ObservedReducer {
935
954
  const loc = ev.readPoint?.id ?? '';
936
955
  /* 변환의 마스터데이터는 **출력**에 적용된다(§7.3.8) — 입력에 붙이면 소비되는 것에 태생을 심는 셈. */
937
956
  this.touchLocation(loc);
957
+ /* 다른 렌즈의 자리에서 나온 출력은 그 렌즈의 것이다 — 이 렌즈의 재고에 세우지 않는다. */
958
+ if (this.referenced.has(loc))
959
+ return;
938
960
  for (const epc of ev.outputEPCList ?? []) {
939
961
  this.items.set(epc, this.mergeItem(epc, { location: loc, disposition: ev.disposition, ilmd: ev.ilmd }));
940
962
  }
@@ -993,6 +1015,18 @@ export class ObservedReducer {
993
1015
  const all = ev.quantityList ?? [];
994
1016
  const q = all[0];
995
1017
  this.touchLocation(loc);
1018
+ /*
1019
+ * **다른 렌즈의 자리로 갔다** — 그 물품은 이 렌즈를 떠났다. 개체는 지우고, 수량은 거기 앉히지 않는다(떠난 쪽의 줄은
1020
+ * 원본의 「없어졌다」가 줄인다). 순서 판정은 그대로 지난다 — 늦게 온 옛 불출이 그 뒤에 돌아온 물품을 지우지 않게.
1021
+ */
1022
+ if (loc && this.referenced.has(loc)) {
1023
+ for (const epc of ev.epcList) {
1024
+ if (envelope && this.stale(`item:${epc}`, envelope))
1025
+ continue;
1026
+ this.remove(epc);
1027
+ }
1028
+ return;
1029
+ }
996
1030
  for (const epc of ev.epcList) {
997
1031
  /* 물품별 순서 판정 — 늦게 온 옛 관측이 최신 위치를 덮지 않게. */
998
1032
  if (envelope && this.stale(`item:${epc}`, envelope))
@@ -1409,6 +1443,18 @@ export class ObservedReducer {
1409
1443
  this.corrections = (cp?.corrections ?? []).map(c => ({ ...c }));
1410
1444
  this.unhandled = new Map((cp?.unhandled ?? []).map(({ eventType, ...v }) => [eventType, { ...v }]));
1411
1445
  }
1446
+ /**
1447
+ * **이어받은 접힌 쪽을 앉힌다**(ADR-0092) — 이어가기 씨앗으로 뜨는 미러가 부른다(§`FlowEngine.hydrateContinuity`).
1448
+ *
1449
+ * 이미 접힌 것이 있으면 **건드리지 않는다.** 재개점(체크포인트)이 먼저 되세웠다면 그것이 정본이고, 같은 값을 또 더하면 세는
1450
+ * 수가 두 번 는다. 비어 있을 때만 이어받는다. 이어받았으면 참을 돌려준다.
1451
+ */
1452
+ adoptFoldedOrders(folded) {
1453
+ if (foldedOrderCount(this.foldedOrders))
1454
+ return false;
1455
+ this.foldedOrders = structuredClone(folded);
1456
+ return true;
1457
+ }
1412
1458
  /**
1413
1459
  * **끝난 오더를 접는다**(ADR-0092) — 접은 수를 돌려준다.
1414
1460
  *
@@ -1479,6 +1525,7 @@ export class ObservedReducer {
1479
1525
  equipment: [...this.equipment.values()].map(m => ({ ...m, ...this.oeePart(m.id), ...this.effectivityPart(m), ...this.offShiftPart(`eq:${m.id}`), ...this.capabilityPart(m, `eq:${m.id}`, m.kind ? [m.kind] : [], this.classDefs.equipment) })),
1480
1526
  orders: [...this.orders.values()].map(o => ({ ...o })),
1481
1527
  ...(foldedOrderCount(this.foldedOrders) ? { foldedOrders: structuredClone(this.foldedOrders) } : {}),
1528
+ ...(this.referencedDecl.length ? { referencedLocations: this.referencedDecl.map(r => ({ ...r })) } : {}),
1482
1529
  acked: [...this.acked]
1483
1530
  };
1484
1531
  }
@@ -868,6 +868,7 @@ var ALLOCATION_POLICY = {
868
868
  /** 먼저 만료되는 것부터(First Expired, First Out). */
869
869
  fefo: "fefo"
870
870
  };
871
+ var BASIS_UNKNOWN = ["BEFORE_BASELINE"];
871
872
  var CMD = {
872
873
  orderHold: "order.hold",
873
874
  orderResume: "order.resume",
@@ -2035,6 +2036,11 @@ function pruneFoldedHours(folded, nowMs, policy = ORDER_FOLD_DEFAULT) {
2035
2036
  return out;
2036
2037
  }
2037
2038
 
2039
+ // ../ops-contract/dist/referenced-location.js
2040
+ function referencedLocationIds(def) {
2041
+ return new Set((def?.referencedLocations ?? []).map((r) => r.id).filter(Boolean));
2042
+ }
2043
+
2038
2044
  // ../ops-contract/dist/iso-duration.js
2039
2045
  var RE = /^(-)?P(?:(\d+(?:\.\d+)?)W)?(?:(\d+(?:\.\d+)?)D)?(?:T(?:(\d+(?:\.\d+)?)H)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)S)?)?$/;
2040
2046
  function parseIsoDuration(text) {
@@ -2299,8 +2305,12 @@ var SPECS = {
2299
2305
  */
2300
2306
  recipeKey: "string",
2301
2307
  recipeBasis: "object",
2302
- materialBasis: "object[]"
2308
+ materialBasis: "object[]",
2309
+ /* 근거 칸이 빈 까닭 — 닫힌 낱말이다(§`BASIS_UNKNOWN`). 투입이 근거 장치보다 먼저였던 오더도 이행은 사실이다. */
2310
+ basisUnknown: "string"
2303
2311
  },
2312
+ /* 상태·종류는 도메인이 소유한다 — 닫지 않는다. 근거가 빈 까닭은 커널이 읽으므로 닫는다. */
2313
+ enums: { basisUnknown: BASIS_UNKNOWN },
2304
2314
  /*
2305
2315
  * 확보분의 안쪽을 본다 — 열쇠와 수 둘이다(§`AllocatedQuantity`). 안 보면 틀린 이름이 조용히
2306
2316
  * 지나가고(`epcs`·`quantity` 따위), 미러는 잡힌 수를 0 으로 읽는다.
@@ -2322,7 +2332,6 @@ var SPECS = {
2322
2332
  positive: ["qty"]
2323
2333
  }
2324
2334
  }
2325
- /* 상태·종류는 도메인이 소유한다 — 닫지 않는다. */
2326
2335
  },
2327
2336
  quality: {
2328
2337
  eventType: OP_EVENT.quality,
@@ -2997,6 +3006,12 @@ var ObservedReducer = class {
2997
3006
  classDefs = {};
2998
3007
  /** 시각 해석 기준(트윈 모델 선언) — 없으면 UTC. 캘린더의 `HH:MM` 이 어느 기준인지 정한다. */
2999
3008
  utcOffsetMinutes;
3009
+ /*
3010
+ * **다른 렌즈의 자리** — 참조만 한다(ADR-0081 보탬). 관측으로 자리로 올리지 않고, 거기로 간 물품은 이 렌즈의 재고에서 뺀다.
3011
+ * 창고가 조립 라인으로 불출한 자재는 창고 재고가 아니다 — 그 자리를 「종류 모르는 자리」로 세우면 재고가 거기 남는다.
3012
+ */
3013
+ referencedDecl = [];
3014
+ referenced = /* @__PURE__ */ new Set();
3000
3015
  constructor(model) {
3001
3016
  const fold = orderFoldPolicyOf(model.orderFold);
3002
3017
  if ("error" in fold) throw new Error(fold.error);
@@ -3004,6 +3019,8 @@ var ObservedReducer = class {
3004
3019
  this.utcOffsetMinutes = model.utcOffsetMinutes;
3005
3020
  this.classDefs = { personnel: model.personnelClasses, equipment: model.equipmentClasses, asset: model.assetClasses };
3006
3021
  for (const sp of model.testSpecifications ?? []) this.testSpecs.set(sp.id, sp);
3022
+ this.referencedDecl = [...model.referencedLocations ?? []];
3023
+ this.referenced = referencedLocationIds(model);
3007
3024
  for (const n of readBoardLocations(model)) this.master.set(n.id, { id: n.id, type: n.type, capacity: n.capacity, parallelism: n.parallelism, parentId: n.parentId, origin: "master" });
3008
3025
  for (const m of readBoardEquipment(model)) this.equipment.set(m.id, { id: m.id, kind: m.kind, ...m.equipmentClassIds ? { equipmentClassIds: m.equipmentClassIds } : {}, status: "idle", location: m.homeLocation, homeLocation: m.homeLocation, ...m.properties ? { properties: m.properties } : {}, ...m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}, ...m.testResults ? { testResults: m.testResults } : {}, ...effectiveOf(m), origin: "master" });
3009
3026
  for (const p of model.persons ?? []) this.persons.set(p.id, { id: p.id, personnelClassIds: p.personnelClassIds, status: "idle", ...p.homeLocation ? { location: p.homeLocation } : {}, ...p.properties ? { properties: p.properties } : {}, ...p.testSpecificationIds ? { testSpecificationIds: p.testSpecificationIds } : {}, ...p.testResults ? { testResults: p.testResults } : {}, ...effectiveOf(p) });
@@ -3035,6 +3052,14 @@ var ObservedReducer = class {
3035
3052
  const declaredLocationIds = new Set(locations.map((n) => n.id));
3036
3053
  let locationsAdded = 0;
3037
3054
  let locationsDropped = 0;
3055
+ this.referencedDecl = [...model.referencedLocations ?? []];
3056
+ this.referenced = referencedLocationIds(model);
3057
+ for (const [id, cur] of [...this.master]) {
3058
+ if (cur.origin === "observed" && this.referenced.has(id)) this.master.delete(id);
3059
+ }
3060
+ for (const [key, it] of [...this.items]) {
3061
+ if (it.location && this.referenced.has(it.location)) this.remove(key);
3062
+ }
3038
3063
  for (const [id, cur] of [...this.master]) {
3039
3064
  if (declaredLocationIds.has(id) || cur.origin === "observed") continue;
3040
3065
  this.master.delete(id);
@@ -3105,7 +3130,7 @@ var ObservedReducer = class {
3105
3130
  * (트윈 모델에 좌표가 없고, 용량을 채워야 계획에 참여한다). 마스터 동기가 오면 `master` 로 승격된다.
3106
3131
  */
3107
3132
  touchLocation(id) {
3108
- if (!id || this.master.has(id)) return;
3133
+ if (!id || this.master.has(id) || this.referenced.has(id)) return;
3109
3134
  this.master.set(id, { id, type: UNKNOWN_TYPE, origin: "observed" });
3110
3135
  }
3111
3136
  /**
@@ -3552,6 +3577,7 @@ var ObservedReducer = class {
3552
3577
  for (const epc of ev.inputEPCList ?? []) this.remove(epc);
3553
3578
  const loc2 = ev.readPoint?.id ?? "";
3554
3579
  this.touchLocation(loc2);
3580
+ if (this.referenced.has(loc2)) return;
3555
3581
  for (const epc of ev.outputEPCList ?? []) {
3556
3582
  this.items.set(epc, this.mergeItem(epc, { location: loc2, disposition: ev.disposition, ilmd: ev.ilmd }));
3557
3583
  }
@@ -3580,6 +3606,13 @@ var ObservedReducer = class {
3580
3606
  const all = ev.quantityList ?? [];
3581
3607
  const q = all[0];
3582
3608
  this.touchLocation(loc);
3609
+ if (loc && this.referenced.has(loc)) {
3610
+ for (const epc of ev.epcList) {
3611
+ if (envelope && this.stale(`item:${epc}`, envelope)) continue;
3612
+ this.remove(epc);
3613
+ }
3614
+ return;
3615
+ }
3583
3616
  for (const epc of ev.epcList) {
3584
3617
  if (envelope && this.stale(`item:${epc}`, envelope)) continue;
3585
3618
  const seen = this.mergeItem(epc, { location: loc, disposition: ev.disposition, ilmd: ev.ilmd }, q, all);
@@ -3904,6 +3937,17 @@ var ObservedReducer = class {
3904
3937
  this.corrections = (cp?.corrections ?? []).map((c) => ({ ...c }));
3905
3938
  this.unhandled = new Map((cp?.unhandled ?? []).map(({ eventType, ...v }) => [eventType, { ...v }]));
3906
3939
  }
3940
+ /**
3941
+ * **이어받은 접힌 쪽을 앉힌다**(ADR-0092) — 이어가기 씨앗으로 뜨는 미러가 부른다(§`FlowEngine.hydrateContinuity`).
3942
+ *
3943
+ * 이미 접힌 것이 있으면 **건드리지 않는다.** 재개점(체크포인트)이 먼저 되세웠다면 그것이 정본이고, 같은 값을 또 더하면 세는
3944
+ * 수가 두 번 는다. 비어 있을 때만 이어받는다. 이어받았으면 참을 돌려준다.
3945
+ */
3946
+ adoptFoldedOrders(folded) {
3947
+ if (foldedOrderCount(this.foldedOrders)) return false;
3948
+ this.foldedOrders = structuredClone(folded);
3949
+ return true;
3950
+ }
3907
3951
  /**
3908
3952
  * **끝난 오더를 접는다**(ADR-0092) — 접은 수를 돌려준다.
3909
3953
  *
@@ -3970,6 +4014,7 @@ var ObservedReducer = class {
3970
4014
  equipment: [...this.equipment.values()].map((m) => ({ ...m, ...this.oeePart(m.id), ...this.effectivityPart(m), ...this.offShiftPart(`eq:${m.id}`), ...this.capabilityPart(m, `eq:${m.id}`, m.kind ? [m.kind] : [], this.classDefs.equipment) })),
3971
4015
  orders: [...this.orders.values()].map((o) => ({ ...o })),
3972
4016
  ...foldedOrderCount(this.foldedOrders) ? { foldedOrders: structuredClone(this.foldedOrders) } : {},
4017
+ ...this.referencedDecl.length ? { referencedLocations: this.referencedDecl.map((r) => ({ ...r })) } : {},
3973
4018
  acked: [...this.acked]
3974
4019
  };
3975
4020
  }
@@ -4878,6 +4923,14 @@ var FlowEngine = class {
4878
4923
  hydrateContinuity(snap) {
4879
4924
  for (const id of snap?.acked ?? []) this._acked.add(id);
4880
4925
  for (const e of snap?.attentionSince ?? []) if (e?.id && e.since) this._attentionSince.set(e.id, e.since);
4926
+ if (snap?.foldedOrders && foldedOrderCount(snap.foldedOrders)) {
4927
+ if (this.observeMode) {
4928
+ if (!this.observer) this.observer = new ObservedReducer(this.boardDef ?? { locations: [], equipment: [] });
4929
+ if (this.observer.adoptFoldedOrders(snap.foldedOrders)) this.observedDirty = true;
4930
+ } else if (!foldedOrderCount(this.foldedOrders)) {
4931
+ this.foldedOrders = structuredClone(snap.foldedOrders);
4932
+ }
4933
+ }
4881
4934
  }
4882
4935
  /**
4883
4936
  * 씨앗의 확보분 중 **이 트윈에 실제로 있는 것만** 남긴다 — 없는 것은 세고 버린다.
@@ -5530,6 +5583,8 @@ var FlowEngine = class {
5530
5583
  })),
5531
5584
  /* 접힌 오더 — 접은 것이 없으면 싣지 않는다(ADR-0092). */
5532
5585
  ...foldedOrderCount(this.foldedOrders) ? { foldedOrders: structuredClone(this.foldedOrders) } : {},
5586
+ /* 다른 렌즈의 자리 — 모델의 선언 그대로(ADR-0081 보탬). 시뮬과 미러가 같은 칸을 낸다. */
5587
+ ...this.boardDef?.referencedLocations?.length ? { referencedLocations: this.boardDef.referencedLocations.map((r) => ({ ...r })) } : {},
5533
5588
  orders: [...this.orders.values()].map((o) => ({
5534
5589
  id: o.id,
5535
5590
  kind: o.kind,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/twin-kernel",
3
- "version": "0.11.27",
3
+ "version": "0.11.29",
4
4
  "type": "module",
5
5
  "description": "Twin Domain Kernel — framework-agnostic, zero-dep (domain + sim + 3-channel contract). WMS/YMS/MES, EPCIS 2.0 · ISA-95.",
6
6
  "publishConfig": {
@@ -28,6 +28,6 @@
28
28
  "test": "node --test test/*.test.ts"
29
29
  },
30
30
  "dependencies": {
31
- "@operato/ops-contract": "^0.9.38"
31
+ "@operato/ops-contract": "^0.9.40"
32
32
  }
33
33
  }