@operato/twin-kernel 0.7.41 → 0.7.42

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.
@@ -59,7 +59,37 @@ export interface AggregationEventMapping {
59
59
  readPoint?: MapValue;
60
60
  bizLocation?: MapValue;
61
61
  }
62
- export type EventMapping = ObjectEventMapping | AggregationEventMapping;
62
+ /**
63
+ * **변환의 사실** — 무엇이 들어가 무엇이 나왔나(EPCIS `TransformationEvent`).
64
+ *
65
+ * ── 왜 이 갈래가 필요한가 (2026-08-21) ─────────────────────────────────────
66
+ * 정규 레코드 계약이 `ObjectEvent`·`AggregationEvent` 둘만 다뤄서, 원본이 「이 로트를 소비해 이 제품을
67
+ * 만들었다」고 말해도 실을 자리가 없었다. 그 사실이 문 앞에서 사라진다.
68
+ *
69
+ * **HACCP 회수 범위 판정이 정확히 이 자리를 요구한다** — 「이 로트가 어느 제품에 들어갔나」다. 로트를
70
+ * 개체로 식별할 수 없는 현장(연속량·분할 로트)에서는 소비를 `inputQuantityList` 의 **LGTIN 클래스**
71
+ * (`urn:epc:class:lgtin:<프리픽스>.<품번>.<로트>`)로 말한다 — 표준이 로트 단위 클래스를 그렇게 정한다.
72
+ *
73
+ * `action` 이 없다: 표준이 `TransformationEvent` 에 `action` 을 두지 않는다(들어감과 나옴이 곧 뜻이다).
74
+ */
75
+ export interface TransformationEventMapping {
76
+ type: 'TransformationEvent';
77
+ bizStep: MapValue;
78
+ disposition?: MapValue;
79
+ /** 소비된 개체들("$.inputEPCList") — 문자열 배열. */
80
+ inputEPCList?: MapValue;
81
+ /** 소비된 클래스+수량("$.inputQuantityList") — 로트 단위 소비가 이 자리다. */
82
+ inputQuantityList?: MapValue;
83
+ /** 산출된 개체들("$.outputEPCList"). */
84
+ outputEPCList?: MapValue;
85
+ /** 산출된 클래스+수량("$.outputQuantityList"). */
86
+ outputQuantityList?: MapValue;
87
+ /** 여러 이벤트를 한 변환으로 잇는 식별자("$.transformationID") — 한 오더의 여러 단계가 이것으로 묶인다. */
88
+ transformationID?: MapValue;
89
+ readPoint?: MapValue;
90
+ bizLocation?: MapValue;
91
+ }
92
+ export type EventMapping = ObjectEventMapping | AggregationEventMapping | TransformationEventMapping;
63
93
  /**
64
94
  * 이 품목 레코드가 **담김의 사실**인가 — 라우팅 판정을 한 곳에 둔다(소비처가 각자 짐작하지 않게).
65
95
  *
@@ -67,6 +97,16 @@ export type EventMapping = ObjectEventMapping | AggregationEventMapping;
67
97
  * 있으면 담김으로 본다 — 둘 다 있으면 커넥터가 무엇을 말하려는지 알 수 없으므로 담김으로 받지 않는다.
68
98
  */
69
99
  export declare function isAggregationRecord(record: unknown): boolean;
100
+ /**
101
+ * 이 레코드가 **변환의 사실**인가 — 라우팅 판정을 한 곳에 둔다(소비처가 각자 짐작하지 않게).
102
+ *
103
+ * 변환은 **들어간 것과 나온 것**으로 말한다. 개체 하나의 관측(`epc`)이나 담김(`parentID`)과 섞이지
104
+ * 않게, 그 둘이 없고 입력·출력 중 하나라도 있으면 변환으로 본다.
105
+ *
106
+ * 한쪽만 있는 것도 변환이다 — 표준이 소실(N→0)과 생성(0→N)을 같은 이벤트로 표현한다. 둘 다 없으면
107
+ * 변환이 아니다(무엇이 바뀌었는지 말하지 않은 것이다).
108
+ */
109
+ export declare function isTransformationRecord(record: unknown): boolean;
70
110
  /** 룰: 소스 레코드의 판별자(sourceType)로 매핑을 고른다. */
71
111
  export interface AdapterRule {
72
112
  sourceType: string;
@@ -8,7 +8,7 @@
8
8
  * 매핑 엔진은 walking-skeleton 범위에서 무의존 declarative spec 사용.
9
9
  * (jsonata/템플릿 엔진 최종 선택은 열린 결정 — face2-adapters.md. 커널 zero-dep 유지 위해 여기선 미도입.)
10
10
  */
11
- import { aggregationEvent, objectEvent, validateEpcisEvent } from "./epcis.js";
11
+ import { aggregationEvent, objectEvent, transformationEvent, validateEpcisEvent } from "./epcis.js";
12
12
  /**
13
13
  * 이 품목 레코드가 **담김의 사실**인가 — 라우팅 판정을 한 곳에 둔다(소비처가 각자 짐작하지 않게).
14
14
  *
@@ -21,6 +21,24 @@ export function isAggregationRecord(record) {
21
21
  const r = record;
22
22
  return typeof r.parentID === 'string' && r.parentID.trim().length > 0 && r.epc === undefined;
23
23
  }
24
+ /**
25
+ * 이 레코드가 **변환의 사실**인가 — 라우팅 판정을 한 곳에 둔다(소비처가 각자 짐작하지 않게).
26
+ *
27
+ * 변환은 **들어간 것과 나온 것**으로 말한다. 개체 하나의 관측(`epc`)이나 담김(`parentID`)과 섞이지
28
+ * 않게, 그 둘이 없고 입력·출력 중 하나라도 있으면 변환으로 본다.
29
+ *
30
+ * 한쪽만 있는 것도 변환이다 — 표준이 소실(N→0)과 생성(0→N)을 같은 이벤트로 표현한다. 둘 다 없으면
31
+ * 변환이 아니다(무엇이 바뀌었는지 말하지 않은 것이다).
32
+ */
33
+ export function isTransformationRecord(record) {
34
+ if (!record || typeof record !== 'object')
35
+ return false;
36
+ const r = record;
37
+ if (r.epc !== undefined || r.parentID !== undefined)
38
+ return false;
39
+ const has = (k) => Array.isArray(r[k]) && r[k].length > 0;
40
+ return has('inputEPCList') || has('inputQuantityList') || has('outputEPCList') || has('outputQuantityList');
41
+ }
24
42
  function get(obj, path) {
25
43
  return path.split('.').reduce((o, k) => (o == null ? o : o[k]), obj);
26
44
  }
@@ -83,12 +101,38 @@ function resolveQuantityList(v, record, field, errors) {
83
101
  /** 단일 레코드 → 정규 EPCIS 이벤트 + 매핑에서 드러난 문제(검증은 `ingest` 가 이어서 한다). */
84
102
  export function mapRecordChecked(record, mapping, eventTime) {
85
103
  const errors = [];
86
- const action = (resolve(mapping.action, record, 'action', errors) ?? '');
87
104
  const bizStep = resolve(mapping.bizStep, record, 'bizStep', errors) ?? '';
88
105
  const disposition = resolve(mapping.disposition, record, 'disposition', errors);
89
106
  const readPoint = resolve(mapping.readPoint, record, 'readPoint', errors);
90
107
  const bizLocation = resolve(mapping.bizLocation, record, 'bizLocation', errors);
108
+ if (mapping.type === 'TransformationEvent') {
109
+ const inputEPCList = resolveList(mapping.inputEPCList, record, 'inputEPCList', errors);
110
+ const inputQuantityList = resolveQuantityList(mapping.inputQuantityList, record, 'inputQuantityList', errors);
111
+ const outputEPCList = resolveList(mapping.outputEPCList, record, 'outputEPCList', errors);
112
+ const outputQuantityList = resolveQuantityList(mapping.outputQuantityList, record, 'outputQuantityList', errors);
113
+ const transformationID = resolve(mapping.transformationID, record, 'transformationID', errors);
114
+ /* 들어간 것도 나온 것도 없으면 무엇이 바뀌었는지 말하지 않은 것이다 — 검증에 넘기기 전에 여기서
115
+ 말해 준다(검증 메시지는 이벤트를 가리키고, 이 메시지는 **매핑**을 가리킨다). */
116
+ if (!inputEPCList.length && !inputQuantityList.length && !outputEPCList.length && !outputQuantityList.length) {
117
+ errors.push('입력과 출력이 모두 비었다 — 변환은 들어간 것이나 나온 것 중 하나는 말해야 한다');
118
+ }
119
+ return {
120
+ event: transformationEvent({
121
+ eventTime, bizStep, disposition,
122
+ /* 없는 쪽은 **필드를 만들지 않는다** — 빈 배열을 실으면 「없다」고 말하는 것이 된다.
123
+ 소실(N→0)과 생성(0→N)은 표준이 인정하는 변환이고, 그때 한쪽이 아예 없는 것이 사실이다. */
124
+ ...(inputEPCList.length ? { inputEPCList } : {}),
125
+ ...(inputQuantityList.length ? { inputQuantityList } : {}),
126
+ ...(outputEPCList.length ? { outputEPCList } : {}),
127
+ ...(outputQuantityList.length ? { outputQuantityList } : {}),
128
+ ...(transformationID ? { transformationID } : {}),
129
+ readPoint, bizLocation
130
+ }),
131
+ errors
132
+ };
133
+ }
91
134
  if (mapping.type === 'AggregationEvent') {
135
+ const action = (resolve(mapping.action, record, 'action', errors) ?? '');
92
136
  const parentID = resolve(mapping.parentID, record, 'parentID', errors) ?? '';
93
137
  const childEPCs = resolveList(mapping.childEPCs, record, 'childEPCs', errors);
94
138
  const childQuantityList = resolveQuantityList(mapping.childQuantityList, record, 'childQuantityList', errors);
@@ -103,6 +147,7 @@ export function mapRecordChecked(record, mapping, eventTime) {
103
147
  errors
104
148
  };
105
149
  }
150
+ const action = (resolve(mapping.action, record, 'action', errors) ?? '');
106
151
  const epc = resolve(mapping.epc, record, 'epc', errors);
107
152
  const quantityList = resolveQuantityList(mapping.quantityList, record, 'quantityList', errors);
108
153
  /* 개체도 수량도 없으면 무엇을 관측했는지 말하지 않은 것이다 — 검증에 넘기기 전에 여기서 말해 준다
@@ -2084,6 +2084,13 @@ export class FlowEngine {
2084
2084
  * 시뮬레이션에서 이것은 **우리 계산의 결함**이다(우리가 없는 물건을 소비하라고 요구했다). 그래서
2085
2085
  * 멈춘다. 미러는 처방이 다르다 — 원본이 진실이므로 받아들이고 어긋난 횟수를 남긴다.
2086
2086
  *
2087
+ * ── 자리 규칙은 **여기서 확인하지 않는다** (2026-08-21에 호출부를 확인한 결과) ────
2088
+ * 「자재는 작업이 일어나는 자리에 있어야 소비된다」는 규칙이 있고, 그 확인은 **소비 경로**
2089
+ * (`claimMaterials`)가 이미 한다(그 자리에 있는 것만 선택한다). 이 원시에 같은 확인을 추가하면
2090
+ * 정상 경로를 막는다: MES 선언 경로는 자재를 **라인사이드 보관 자리**에서 선택하고 변환은
2091
+ * **공정 자리**에서 일어난다(`mes-kernel` 의 라우트 단계 — 두 자리가 다른 것이 그 경로의 설계다).
2092
+ * 추가하려면 먼저 그 경로가 물건을 공정 자리로 이동시키게 만들어야 하고, 그것은 별개의 결정이다.
2093
+ *
2087
2094
  * ── 미러를 멈추면 안 되는 이유 (2026-08-20, 시험으로 확인) ─────────────────
2088
2095
  * 씨앗으로 세운 커널은 그대로 `tick` 한다(§`hydrateObserved`). 원본이 확보분 하나를 누락한
2089
2096
  * 스냅샷을 주면 — 실 연동에서 흔한 빈틈이다 — 그 커널은 **첫 틱에 멈춘다.** 원본의 한 건이
@@ -125,6 +125,7 @@ __export(index_exports, {
125
125
  isEnergyRecord: () => isEnergyRecord,
126
126
  isEquipmentLevel: () => isEquipmentLevel,
127
127
  isOperationalRecord: () => isOperationalRecord,
128
+ isTransformationRecord: () => isTransformationRecord,
128
129
  isoDurationHours: () => isoDurationHours,
129
130
  itemKeyOf: () => itemKeyOf,
130
131
  levelOfLocationType: () => levelOfLocationType,
@@ -2807,6 +2808,13 @@ function isAggregationRecord(record) {
2807
2808
  const r = record;
2808
2809
  return typeof r.parentID === "string" && r.parentID.trim().length > 0 && r.epc === void 0;
2809
2810
  }
2811
+ function isTransformationRecord(record) {
2812
+ if (!record || typeof record !== "object") return false;
2813
+ const r = record;
2814
+ if (r.epc !== void 0 || r.parentID !== void 0) return false;
2815
+ const has = (k) => Array.isArray(r[k]) && r[k].length > 0;
2816
+ return has("inputEPCList") || has("inputQuantityList") || has("outputEPCList") || has("outputQuantityList");
2817
+ }
2810
2818
  function get(obj, path) {
2811
2819
  return path.split(".").reduce((o, k) => o == null ? o : o[k], obj);
2812
2820
  }
@@ -2843,19 +2851,46 @@ function resolveQuantityList(v, record, field, errors) {
2843
2851
  }
2844
2852
  function mapRecordChecked(record, mapping, eventTime) {
2845
2853
  const errors = [];
2846
- const action = resolve(mapping.action, record, "action", errors) ?? "";
2847
2854
  const bizStep = resolve(mapping.bizStep, record, "bizStep", errors) ?? "";
2848
2855
  const disposition = resolve(mapping.disposition, record, "disposition", errors);
2849
2856
  const readPoint = resolve(mapping.readPoint, record, "readPoint", errors);
2850
2857
  const bizLocation = resolve(mapping.bizLocation, record, "bizLocation", errors);
2858
+ if (mapping.type === "TransformationEvent") {
2859
+ const inputEPCList = resolveList(mapping.inputEPCList, record, "inputEPCList", errors);
2860
+ const inputQuantityList = resolveQuantityList(mapping.inputQuantityList, record, "inputQuantityList", errors);
2861
+ const outputEPCList = resolveList(mapping.outputEPCList, record, "outputEPCList", errors);
2862
+ const outputQuantityList = resolveQuantityList(mapping.outputQuantityList, record, "outputQuantityList", errors);
2863
+ const transformationID = resolve(mapping.transformationID, record, "transformationID", errors);
2864
+ if (!inputEPCList.length && !inputQuantityList.length && !outputEPCList.length && !outputQuantityList.length) {
2865
+ errors.push("\uC785\uB825\uACFC \uCD9C\uB825\uC774 \uBAA8\uB450 \uBE44\uC5C8\uB2E4 \u2014 \uBCC0\uD658\uC740 \uB4E4\uC5B4\uAC04 \uAC83\uC774\uB098 \uB098\uC628 \uAC83 \uC911 \uD558\uB098\uB294 \uB9D0\uD574\uC57C \uD55C\uB2E4");
2866
+ }
2867
+ return {
2868
+ event: transformationEvent({
2869
+ eventTime,
2870
+ bizStep,
2871
+ disposition,
2872
+ /* 없는 쪽은 **필드를 만들지 않는다** — 빈 배열을 실으면 「없다」고 말하는 것이 된다.
2873
+ 소실(N→0)과 생성(0→N)은 표준이 인정하는 변환이고, 그때 한쪽이 아예 없는 것이 사실이다. */
2874
+ ...inputEPCList.length ? { inputEPCList } : {},
2875
+ ...inputQuantityList.length ? { inputQuantityList } : {},
2876
+ ...outputEPCList.length ? { outputEPCList } : {},
2877
+ ...outputQuantityList.length ? { outputQuantityList } : {},
2878
+ ...transformationID ? { transformationID } : {},
2879
+ readPoint,
2880
+ bizLocation
2881
+ }),
2882
+ errors
2883
+ };
2884
+ }
2851
2885
  if (mapping.type === "AggregationEvent") {
2886
+ const action2 = resolve(mapping.action, record, "action", errors) ?? "";
2852
2887
  const parentID = resolve(mapping.parentID, record, "parentID", errors) ?? "";
2853
2888
  const childEPCs = resolveList(mapping.childEPCs, record, "childEPCs", errors);
2854
2889
  const childQuantityList = resolveQuantityList(mapping.childQuantityList, record, "childQuantityList", errors);
2855
2890
  return {
2856
2891
  event: aggregationEvent({
2857
2892
  eventTime,
2858
- action,
2893
+ action: action2,
2859
2894
  bizStep,
2860
2895
  disposition,
2861
2896
  parentID,
@@ -2868,6 +2903,7 @@ function mapRecordChecked(record, mapping, eventTime) {
2868
2903
  errors
2869
2904
  };
2870
2905
  }
2906
+ const action = resolve(mapping.action, record, "action", errors) ?? "";
2871
2907
  const epc = resolve(mapping.epc, record, "epc", errors);
2872
2908
  const quantityList = resolveQuantityList(mapping.quantityList, record, "quantityList", errors);
2873
2909
  if (!epc && !quantityList.length) {
@@ -8367,6 +8403,7 @@ function retiredVocabularyIn(line) {
8367
8403
  isEnergyRecord,
8368
8404
  isEquipmentLevel,
8369
8405
  isOperationalRecord,
8406
+ isTransformationRecord,
8370
8407
  isoDurationHours,
8371
8408
  itemKeyOf,
8372
8409
  levelOfLocationType,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/twin-kernel",
3
- "version": "0.7.41",
3
+ "version": "0.7.42",
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": {