@operato/twin-kernel 0.7.48 → 0.7.49

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.
@@ -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
  *
@@ -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
  * 참조를 두었기 때문이다(개체는 "이 사람이 통과했다", 등급은 "이 자격은 이 시험을 요구한다").
@@ -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
  /*
@@ -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
  /*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/twin-kernel",
3
- "version": "0.7.48",
3
+ "version": "0.7.49",
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": {