@operato/twin-kernel 0.11.28 → 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.
@@ -1902,6 +1902,8 @@ export class FlowEngine {
1902
1902
  })),
1903
1903
  /* 접힌 오더 — 접은 것이 없으면 싣지 않는다(ADR-0092). */
1904
1904
  ...(foldedOrderCount(this.foldedOrders) ? { foldedOrders: structuredClone(this.foldedOrders) } : {}),
1905
+ /* 다른 렌즈의 자리 — 모델의 선언 그대로(ADR-0081 보탬). 시뮬과 미러가 같은 칸을 낸다. */
1906
+ ...(this.boardDef?.referencedLocations?.length ? { referencedLocations: this.boardDef.referencedLocations.map(r => ({ ...r })) } : {}),
1905
1907
  orders: [...this.orders.values()].map(o => ({
1906
1908
  id: o.id, kind: o.kind, status: o.status,
1907
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
  * **구조를 전환한다** — 관측된 사실은 지키고 토폴로지만 새 선언으로 바꾼다.
@@ -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))
@@ -1491,6 +1525,7 @@ export class ObservedReducer {
1491
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) })),
1492
1526
  orders: [...this.orders.values()].map(o => ({ ...o })),
1493
1527
  ...(foldedOrderCount(this.foldedOrders) ? { foldedOrders: structuredClone(this.foldedOrders) } : {}),
1528
+ ...(this.referencedDecl.length ? { referencedLocations: this.referencedDecl.map(r => ({ ...r })) } : {}),
1494
1529
  acked: [...this.acked]
1495
1530
  };
1496
1531
  }
@@ -2036,6 +2036,11 @@ function pruneFoldedHours(folded, nowMs, policy = ORDER_FOLD_DEFAULT) {
2036
2036
  return out;
2037
2037
  }
2038
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
+
2039
2044
  // ../ops-contract/dist/iso-duration.js
2040
2045
  var RE = /^(-)?P(?:(\d+(?:\.\d+)?)W)?(?:(\d+(?:\.\d+)?)D)?(?:T(?:(\d+(?:\.\d+)?)H)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)S)?)?$/;
2041
2046
  function parseIsoDuration(text) {
@@ -3001,6 +3006,12 @@ var ObservedReducer = class {
3001
3006
  classDefs = {};
3002
3007
  /** 시각 해석 기준(트윈 모델 선언) — 없으면 UTC. 캘린더의 `HH:MM` 이 어느 기준인지 정한다. */
3003
3008
  utcOffsetMinutes;
3009
+ /*
3010
+ * **다른 렌즈의 자리** — 참조만 한다(ADR-0081 보탬). 관측으로 자리로 올리지 않고, 거기로 간 물품은 이 렌즈의 재고에서 뺀다.
3011
+ * 창고가 조립 라인으로 불출한 자재는 창고 재고가 아니다 — 그 자리를 「종류 모르는 자리」로 세우면 재고가 거기 남는다.
3012
+ */
3013
+ referencedDecl = [];
3014
+ referenced = /* @__PURE__ */ new Set();
3004
3015
  constructor(model) {
3005
3016
  const fold = orderFoldPolicyOf(model.orderFold);
3006
3017
  if ("error" in fold) throw new Error(fold.error);
@@ -3008,6 +3019,8 @@ var ObservedReducer = class {
3008
3019
  this.utcOffsetMinutes = model.utcOffsetMinutes;
3009
3020
  this.classDefs = { personnel: model.personnelClasses, equipment: model.equipmentClasses, asset: model.assetClasses };
3010
3021
  for (const sp of model.testSpecifications ?? []) this.testSpecs.set(sp.id, sp);
3022
+ this.referencedDecl = [...model.referencedLocations ?? []];
3023
+ this.referenced = referencedLocationIds(model);
3011
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" });
3012
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" });
3013
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) });
@@ -3039,6 +3052,14 @@ var ObservedReducer = class {
3039
3052
  const declaredLocationIds = new Set(locations.map((n) => n.id));
3040
3053
  let locationsAdded = 0;
3041
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
+ }
3042
3063
  for (const [id, cur] of [...this.master]) {
3043
3064
  if (declaredLocationIds.has(id) || cur.origin === "observed") continue;
3044
3065
  this.master.delete(id);
@@ -3109,7 +3130,7 @@ var ObservedReducer = class {
3109
3130
  * (트윈 모델에 좌표가 없고, 용량을 채워야 계획에 참여한다). 마스터 동기가 오면 `master` 로 승격된다.
3110
3131
  */
3111
3132
  touchLocation(id) {
3112
- if (!id || this.master.has(id)) return;
3133
+ if (!id || this.master.has(id) || this.referenced.has(id)) return;
3113
3134
  this.master.set(id, { id, type: UNKNOWN_TYPE, origin: "observed" });
3114
3135
  }
3115
3136
  /**
@@ -3556,6 +3577,7 @@ var ObservedReducer = class {
3556
3577
  for (const epc of ev.inputEPCList ?? []) this.remove(epc);
3557
3578
  const loc2 = ev.readPoint?.id ?? "";
3558
3579
  this.touchLocation(loc2);
3580
+ if (this.referenced.has(loc2)) return;
3559
3581
  for (const epc of ev.outputEPCList ?? []) {
3560
3582
  this.items.set(epc, this.mergeItem(epc, { location: loc2, disposition: ev.disposition, ilmd: ev.ilmd }));
3561
3583
  }
@@ -3584,6 +3606,13 @@ var ObservedReducer = class {
3584
3606
  const all = ev.quantityList ?? [];
3585
3607
  const q = all[0];
3586
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
+ }
3587
3616
  for (const epc of ev.epcList) {
3588
3617
  if (envelope && this.stale(`item:${epc}`, envelope)) continue;
3589
3618
  const seen = this.mergeItem(epc, { location: loc, disposition: ev.disposition, ilmd: ev.ilmd }, q, all);
@@ -3985,6 +4014,7 @@ var ObservedReducer = class {
3985
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) })),
3986
4015
  orders: [...this.orders.values()].map((o) => ({ ...o })),
3987
4016
  ...foldedOrderCount(this.foldedOrders) ? { foldedOrders: structuredClone(this.foldedOrders) } : {},
4017
+ ...this.referencedDecl.length ? { referencedLocations: this.referencedDecl.map((r) => ({ ...r })) } : {},
3988
4018
  acked: [...this.acked]
3989
4019
  };
3990
4020
  }
@@ -5553,6 +5583,8 @@ var FlowEngine = class {
5553
5583
  })),
5554
5584
  /* 접힌 오더 — 접은 것이 없으면 싣지 않는다(ADR-0092). */
5555
5585
  ...foldedOrderCount(this.foldedOrders) ? { foldedOrders: structuredClone(this.foldedOrders) } : {},
5586
+ /* 다른 렌즈의 자리 — 모델의 선언 그대로(ADR-0081 보탬). 시뮬과 미러가 같은 칸을 낸다. */
5587
+ ...this.boardDef?.referencedLocations?.length ? { referencedLocations: this.boardDef.referencedLocations.map((r) => ({ ...r })) } : {},
5556
5588
  orders: [...this.orders.values()].map((o) => ({
5557
5589
  id: o.id,
5558
5590
  kind: o.kind,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/twin-kernel",
3
- "version": "0.11.28",
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
  }