@operato/twin-kernel 0.0.1

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.
Files changed (43) hide show
  1. package/README.md +80 -0
  2. package/dist/allocation-policy.d.ts +48 -0
  3. package/dist/allocation-policy.js +50 -0
  4. package/dist/contract.d.ts +205 -0
  5. package/dist/contract.js +20 -0
  6. package/dist/counterfactual.d.ts +40 -0
  7. package/dist/counterfactual.js +59 -0
  8. package/dist/divergence.d.ts +15 -0
  9. package/dist/divergence.js +28 -0
  10. package/dist/duration-estimator.d.ts +12 -0
  11. package/dist/duration-estimator.js +10 -0
  12. package/dist/epcis.d.ts +125 -0
  13. package/dist/epcis.js +172 -0
  14. package/dist/event-journal.d.ts +17 -0
  15. package/dist/event-journal.js +41 -0
  16. package/dist/face2-adapter.d.ts +42 -0
  17. package/dist/face2-adapter.js +69 -0
  18. package/dist/flow-engine.d.ts +224 -0
  19. package/dist/flow-engine.js +422 -0
  20. package/dist/forecast.d.ts +29 -0
  21. package/dist/forecast.js +34 -0
  22. package/dist/index.d.ts +19 -0
  23. package/dist/index.js +19 -0
  24. package/dist/kernel.d.ts +28 -0
  25. package/dist/kernel.js +187 -0
  26. package/dist/mes-kernel.d.ts +28 -0
  27. package/dist/mes-kernel.js +126 -0
  28. package/dist/mes-profile.d.ts +9 -0
  29. package/dist/mes-profile.js +17 -0
  30. package/dist/runtime.d.ts +42 -0
  31. package/dist/runtime.js +59 -0
  32. package/dist/state-projector.d.ts +37 -0
  33. package/dist/state-projector.js +124 -0
  34. package/dist/twin-observer.d.ts +28 -0
  35. package/dist/twin-observer.js +41 -0
  36. package/dist/wms-profile.d.ts +13 -0
  37. package/dist/wms-profile.js +20 -0
  38. package/dist/yms-kernel.d.ts +29 -0
  39. package/dist/yms-kernel.js +181 -0
  40. package/dist/yms-profile.d.ts +11 -0
  41. package/dist/yms-profile.js +22 -0
  42. package/dist-cjs/index.cjs +1477 -0
  43. package/package.json +30 -0
@@ -0,0 +1,125 @@
1
+ /** EPCIS 2.0 JSON-LD 컨텍스트 — 이벤트를 자기기술적 EPCIS 오브젝트로 만든다. */
2
+ export declare const EPCIS_CONTEXT = "https://ref.gs1.org/standards/epcis/2.0.0/epcis-context.jsonld";
3
+ /** 커널 시계는 UTC(BASE_EPOCH) 기준 → 고정 오프셋. */
4
+ export declare const UTC_OFFSET = "+00:00";
5
+ export declare const DISP: {
6
+ readonly in_progress: "urn:epcglobal:cbv:disp:in_progress";
7
+ readonly sellable: "urn:epcglobal:cbv:disp:sellable_accessible";
8
+ readonly reserved: "urn:epcglobal:cbv:disp:reserved";
9
+ readonly in_transit: "urn:epcglobal:cbv:disp:in_transit";
10
+ readonly non_sellable: "urn:epcglobal:cbv:disp:non_sellable_other";
11
+ };
12
+ export type EpcisEventType = 'ObjectEvent' | 'AggregationEvent' | 'TransactionEvent' | 'TransformationEvent';
13
+ export type EpcisAction = 'ADD' | 'OBSERVE' | 'DELETE';
14
+ export interface QuantityElement {
15
+ epcClass: string;
16
+ quantity: number;
17
+ uom?: string;
18
+ }
19
+ export interface BizTransactionElement {
20
+ type: string;
21
+ bizTransaction: string;
22
+ }
23
+ export interface LocationRef {
24
+ id: string;
25
+ }
26
+ /** EPCIS 2.0 공통 헤더(모든 이벤트). action 은 여기 없음 — TransformationEvent 는 action 이 없다. */
27
+ interface EpcisHeader {
28
+ '@context': string;
29
+ type: EpcisEventType;
30
+ eventTime: string;
31
+ eventTimeZoneOffset: string;
32
+ bizStep: string;
33
+ disposition?: string;
34
+ readPoint?: LocationRef;
35
+ bizLocation?: LocationRef;
36
+ bizTransactionList?: BizTransactionElement[];
37
+ }
38
+ /** action 을 갖는 이벤트(Object/Aggregation/Transaction). */
39
+ interface EpcisActionEvent extends EpcisHeader {
40
+ action: EpcisAction;
41
+ }
42
+ export interface EpcisObjectEvent extends EpcisActionEvent {
43
+ type: 'ObjectEvent';
44
+ epcList: string[];
45
+ quantityList?: QuantityElement[];
46
+ }
47
+ export interface EpcisAggregationEvent extends EpcisActionEvent {
48
+ type: 'AggregationEvent';
49
+ parentID: string;
50
+ childEPCs?: string[];
51
+ childQuantityList?: QuantityElement[];
52
+ }
53
+ export interface EpcisTransactionEvent extends EpcisActionEvent {
54
+ type: 'TransactionEvent';
55
+ bizTransactionList: BizTransactionElement[];
56
+ parentID?: string;
57
+ epcList?: string[];
58
+ quantityList?: QuantityElement[];
59
+ }
60
+ /** 변환(제조): 입력 소비 → 출력 생산. action 없음(EPCIS 2.0). */
61
+ export interface EpcisTransformationEvent extends EpcisHeader {
62
+ type: 'TransformationEvent';
63
+ inputEPCList?: string[];
64
+ inputQuantityList?: QuantityElement[];
65
+ outputEPCList?: string[];
66
+ outputQuantityList?: QuantityElement[];
67
+ transformationID?: string;
68
+ }
69
+ export type EpcisEvent = EpcisObjectEvent | EpcisAggregationEvent | EpcisTransactionEvent | EpcisTransformationEvent;
70
+ /** SSCC (물류단위: 팔레트/화물/트레일러) — 결정적 카운터 기반. */
71
+ export declare function ssccUri(companyPrefix: string, serial: number): string;
72
+ /** 비직렬 수량용 SGTIN 패턴(epcClass) — quantityList 에 사용. */
73
+ export declare function sgtinClass(companyPrefix: string, itemRef: string): string;
74
+ /** 거래문서 식별자 = GDTI (PO/SO/WO/어포인트먼트 등). */
75
+ export declare function gdtiUri(companyPrefix: string, docType: string, serial: number): string;
76
+ export declare function objectEvent(p: {
77
+ eventTime: string;
78
+ action: EpcisAction;
79
+ bizStep: string;
80
+ disposition?: string;
81
+ epcList: string[];
82
+ quantityList?: QuantityElement[];
83
+ readPoint?: string;
84
+ bizLocation?: string;
85
+ bizTransactionList?: BizTransactionElement[];
86
+ }): EpcisObjectEvent;
87
+ export declare function aggregationEvent(p: {
88
+ eventTime: string;
89
+ action: EpcisAction;
90
+ bizStep: string;
91
+ disposition?: string;
92
+ parentID: string;
93
+ childEPCs?: string[];
94
+ childQuantityList?: QuantityElement[];
95
+ readPoint?: string;
96
+ bizLocation?: string;
97
+ }): EpcisAggregationEvent;
98
+ export declare function transactionEvent(p: {
99
+ eventTime: string;
100
+ action: EpcisAction;
101
+ bizStep: string;
102
+ disposition?: string;
103
+ bizTransactionList: BizTransactionElement[];
104
+ parentID?: string;
105
+ epcList?: string[];
106
+ quantityList?: QuantityElement[];
107
+ readPoint?: string;
108
+ bizLocation?: string;
109
+ }): EpcisTransactionEvent;
110
+ /** 변환(제조) — 입력 EPC/수량 소비 → 출력 EPC/수량 생산. action 없음. */
111
+ export declare function transformationEvent(p: {
112
+ eventTime: string;
113
+ bizStep: string;
114
+ disposition?: string;
115
+ inputEPCList?: string[];
116
+ inputQuantityList?: QuantityElement[];
117
+ outputEPCList?: string[];
118
+ outputQuantityList?: QuantityElement[];
119
+ transformationID?: string;
120
+ readPoint?: string;
121
+ bizLocation?: string;
122
+ bizTransactionList?: BizTransactionElement[];
123
+ }): EpcisTransformationEvent;
124
+ export declare function validateEpcisEvent(e: EpcisEvent): string[];
125
+ export {};
package/dist/epcis.js ADDED
@@ -0,0 +1,172 @@
1
+ /*
2
+ * EPCIS — GS1 EPCIS 2.0 이벤트 machinery (도메인-중립). WMS/YMS/MES 모두 이 표준 위에서 방출한다.
3
+ * 타입 + 빌더(Object/Aggregation/Transaction/Transformation) + 무의존 구조 검증기 + GS1 URI + CBV disposition.
4
+ * 도메인별 어휘(bizStep/btt)는 각 프로파일(wms/yms/mes-profile)에 둔다.
5
+ *
6
+ * EPCIS 2.0 JSON 바인딩: 각 이벤트는 @context/type/eventTime/eventTimeZoneOffset 를 포함하는
7
+ * 자기기술적(self-describing) JSON-LD 오브젝트.
8
+ */
9
+ /** EPCIS 2.0 JSON-LD 컨텍스트 — 이벤트를 자기기술적 EPCIS 오브젝트로 만든다. */
10
+ export const EPCIS_CONTEXT = 'https://ref.gs1.org/standards/epcis/2.0.0/epcis-context.jsonld';
11
+ /** 커널 시계는 UTC(BASE_EPOCH) 기준 → 고정 오프셋. */
12
+ export const UTC_OFFSET = '+00:00';
13
+ // disposition — CBV URN (GS1 표준 상태 어휘, 전 도메인 공용).
14
+ export const DISP = {
15
+ in_progress: 'urn:epcglobal:cbv:disp:in_progress',
16
+ sellable: 'urn:epcglobal:cbv:disp:sellable_accessible',
17
+ reserved: 'urn:epcglobal:cbv:disp:reserved',
18
+ in_transit: 'urn:epcglobal:cbv:disp:in_transit',
19
+ non_sellable: 'urn:epcglobal:cbv:disp:non_sellable_other' // 불량/scrap
20
+ };
21
+ // ── GS1 EPC URI 헬퍼 (표준) ────────────────────────────────────────────────
22
+ /** SSCC (물류단위: 팔레트/화물/트레일러) — 결정적 카운터 기반. */
23
+ export function ssccUri(companyPrefix, serial) {
24
+ return `urn:epc:id:sscc:${companyPrefix}.${String(serial).padStart(10, '0')}`;
25
+ }
26
+ /** 비직렬 수량용 SGTIN 패턴(epcClass) — quantityList 에 사용. */
27
+ export function sgtinClass(companyPrefix, itemRef) {
28
+ return `urn:epc:idpat:sgtin:${companyPrefix}.${itemRef}.*`;
29
+ }
30
+ /** 거래문서 식별자 = GDTI (PO/SO/WO/어포인트먼트 등). */
31
+ export function gdtiUri(companyPrefix, docType, serial) {
32
+ return `urn:epc:id:gdti:${companyPrefix}.${docType}.${serial}`;
33
+ }
34
+ // ── 이벤트 빌더 (EPCIS 2.0 정규) ──────────────────────────────────────────
35
+ function header(type, eventTime, bizStep) {
36
+ return { '@context': EPCIS_CONTEXT, type, eventTime, eventTimeZoneOffset: UTC_OFFSET, bizStep };
37
+ }
38
+ function common(type, eventTime, action, bizStep) {
39
+ return { ...header(type, eventTime, bizStep), action };
40
+ }
41
+ export function objectEvent(p) {
42
+ const e = { ...common('ObjectEvent', p.eventTime, p.action, p.bizStep), epcList: p.epcList };
43
+ if (p.disposition)
44
+ e.disposition = p.disposition;
45
+ if (p.quantityList)
46
+ e.quantityList = p.quantityList;
47
+ if (p.readPoint)
48
+ e.readPoint = { id: p.readPoint };
49
+ if (p.bizLocation)
50
+ e.bizLocation = { id: p.bizLocation };
51
+ if (p.bizTransactionList)
52
+ e.bizTransactionList = p.bizTransactionList;
53
+ return e;
54
+ }
55
+ export function aggregationEvent(p) {
56
+ const e = { ...common('AggregationEvent', p.eventTime, p.action, p.bizStep), parentID: p.parentID };
57
+ if (p.disposition)
58
+ e.disposition = p.disposition;
59
+ if (p.childEPCs)
60
+ e.childEPCs = p.childEPCs;
61
+ if (p.childQuantityList)
62
+ e.childQuantityList = p.childQuantityList;
63
+ if (p.readPoint)
64
+ e.readPoint = { id: p.readPoint };
65
+ if (p.bizLocation)
66
+ e.bizLocation = { id: p.bizLocation };
67
+ return e;
68
+ }
69
+ export function transactionEvent(p) {
70
+ const e = {
71
+ ...common('TransactionEvent', p.eventTime, p.action, p.bizStep),
72
+ bizTransactionList: p.bizTransactionList
73
+ };
74
+ if (p.disposition)
75
+ e.disposition = p.disposition;
76
+ if (p.parentID)
77
+ e.parentID = p.parentID;
78
+ if (p.epcList)
79
+ e.epcList = p.epcList;
80
+ if (p.quantityList)
81
+ e.quantityList = p.quantityList;
82
+ if (p.readPoint)
83
+ e.readPoint = { id: p.readPoint };
84
+ if (p.bizLocation)
85
+ e.bizLocation = { id: p.bizLocation };
86
+ return e;
87
+ }
88
+ /** 변환(제조) — 입력 EPC/수량 소비 → 출력 EPC/수량 생산. action 없음. */
89
+ export function transformationEvent(p) {
90
+ const e = header('TransformationEvent', p.eventTime, p.bizStep);
91
+ if (p.disposition)
92
+ e.disposition = p.disposition;
93
+ if (p.inputEPCList)
94
+ e.inputEPCList = p.inputEPCList;
95
+ if (p.inputQuantityList)
96
+ e.inputQuantityList = p.inputQuantityList;
97
+ if (p.outputEPCList)
98
+ e.outputEPCList = p.outputEPCList;
99
+ if (p.outputQuantityList)
100
+ e.outputQuantityList = p.outputQuantityList;
101
+ if (p.transformationID)
102
+ e.transformationID = p.transformationID;
103
+ if (p.readPoint)
104
+ e.readPoint = { id: p.readPoint };
105
+ if (p.bizLocation)
106
+ e.bizLocation = { id: p.bizLocation };
107
+ if (p.bizTransactionList)
108
+ e.bizTransactionList = p.bizTransactionList;
109
+ return e;
110
+ }
111
+ // ── 구조 검증기 (무의존 — 커널 zero-dep 원칙) ──────────────────────────────
112
+ // EPCIS 2.0 이벤트의 필수 필드·형태를 검증. 위반 목록을 반환(빈 배열 = 유효).
113
+ // JSON Schema 파일 파생은 후속 — 이 함수가 제약의 SoT.
114
+ const ISO_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
115
+ const TZ_RE = /^[+-]\d{2}:\d{2}$/;
116
+ const ACTIONS = ['ADD', 'OBSERVE', 'DELETE'];
117
+ export function validateEpcisEvent(e) {
118
+ const v = [];
119
+ if (e['@context'] !== EPCIS_CONTEXT)
120
+ v.push('@context 누락/불일치');
121
+ if (!['ObjectEvent', 'AggregationEvent', 'TransactionEvent', 'TransformationEvent'].includes(e.type))
122
+ v.push(`알 수 없는 type: ${e.type}`);
123
+ if (typeof e.eventTime !== 'string' || !ISO_RE.test(e.eventTime))
124
+ v.push('eventTime ISO8601 아님');
125
+ if (typeof e.eventTimeZoneOffset !== 'string' || !TZ_RE.test(e.eventTimeZoneOffset))
126
+ v.push('eventTimeZoneOffset 형식 오류');
127
+ if (typeof e.bizStep !== 'string' || !e.bizStep)
128
+ v.push('bizStep 누락');
129
+ if (e.type === 'ObjectEvent') {
130
+ if (!ACTIONS.includes(e.action))
131
+ v.push(`action 부정: ${e.action}`);
132
+ if (!Array.isArray(e.epcList))
133
+ v.push('ObjectEvent.epcList 누락');
134
+ else if (e.epcList.length === 0 && !e.quantityList?.length)
135
+ v.push('ObjectEvent: epcList/quantityList 둘 다 비어있음');
136
+ }
137
+ else if (e.type === 'AggregationEvent') {
138
+ if (!ACTIONS.includes(e.action))
139
+ v.push(`action 부정: ${e.action}`);
140
+ if (!e.parentID)
141
+ v.push('AggregationEvent.parentID 누락');
142
+ if (!e.childEPCs?.length && !e.childQuantityList?.length)
143
+ v.push('AggregationEvent: childEPCs/childQuantityList 둘 다 비어있음');
144
+ }
145
+ else if (e.type === 'TransactionEvent') {
146
+ if (!ACTIONS.includes(e.action))
147
+ v.push(`action 부정: ${e.action}`);
148
+ if (!Array.isArray(e.bizTransactionList) || e.bizTransactionList.length === 0)
149
+ v.push('TransactionEvent.bizTransactionList 누락');
150
+ }
151
+ else if (e.type === 'TransformationEvent') {
152
+ if (!e.inputEPCList?.length && !e.inputQuantityList?.length)
153
+ v.push('TransformationEvent: input 비어있음');
154
+ if (!e.outputEPCList?.length && !e.outputQuantityList?.length)
155
+ v.push('TransformationEvent: output 비어있음');
156
+ }
157
+ // 참조 무결성(경량): 모든 수량 리스트의 epcClass 는 idpat/class
158
+ const qtyLists = [
159
+ 'quantityList' in e ? e.quantityList : undefined,
160
+ 'childQuantityList' in e ? e.childQuantityList : undefined,
161
+ 'inputQuantityList' in e ? e.inputQuantityList : undefined,
162
+ 'outputQuantityList' in e ? e.outputQuantityList : undefined
163
+ ];
164
+ for (const list of qtyLists)
165
+ for (const q of list ?? []) {
166
+ if (!q.epcClass?.startsWith('urn:epc:idpat:') && !q.epcClass?.startsWith('urn:epc:class:'))
167
+ v.push(`quantity epcClass 부정: ${q.epcClass}`);
168
+ if (typeof q.quantity !== 'number' || q.quantity < 0)
169
+ v.push('quantity 부정');
170
+ }
171
+ return v;
172
+ }
@@ -0,0 +1,17 @@
1
+ import type { BoardDef, CanonicalEnvelope } from './contract.ts';
2
+ import { type ProjectedState } from './state-projector.ts';
3
+ export declare class EventJournal {
4
+ private events;
5
+ /** 이벤트 1건 기록(추가 전용). runtime/kernel 의 onEvent 에 연결. */
6
+ append(e: CanonicalEnvelope): void;
7
+ get length(): number;
8
+ all(): readonly CanonicalEnvelope[];
9
+ /** 처음 revision 개(= 시점 T까지). revision 은 emit 당 1 증가하므로 slice(0,rev). */
10
+ until(revision: number): CanonicalEnvelope[];
11
+ /** revision 이후(재동기·증분 전송용). */
12
+ since(revision: number): CanonicalEnvelope[];
13
+ /** sim 시각(ISO eventTime) 이하 이벤트. */
14
+ untilSimTime(iso: string): CanonicalEnvelope[];
15
+ }
16
+ /** 이벤트열 → 상태 재구성(시간여행). board = 마스터(토폴로지·무버). */
17
+ export declare function replay(board: BoardDef, events: readonly CanonicalEnvelope[]): ProjectedState;
@@ -0,0 +1,41 @@
1
+ /*
2
+ * Event Journal + Replay — 트윈의 기억(이력·시간여행).
3
+ *
4
+ * 트윈은 자기 이벤트 스트림을 남긴다(내구 로그). projector 가 "이벤트→상태"를 하므로,
5
+ * 저널을 "시점 T까지" 재생하면 그 시점의 상태를 재구성(time-travel) — 감사·리플레이·발산 비교의 기반.
6
+ * 지금은 인메모리(framework-agnostic). things-factory 에선 이 append 를 DB 이벤트 테이블로 교체(멀티DB).
7
+ * (이벤트-소싱: 상태는 항상 이벤트열의 함수. state-projector 가 폴딩 함수.)
8
+ */
9
+ import { StateProjector } from "./state-projector.js";
10
+ export class EventJournal {
11
+ events = [];
12
+ /** 이벤트 1건 기록(추가 전용). runtime/kernel 의 onEvent 에 연결. */
13
+ append(e) {
14
+ this.events.push(e);
15
+ }
16
+ get length() {
17
+ return this.events.length;
18
+ }
19
+ all() {
20
+ return this.events;
21
+ }
22
+ /** 처음 revision 개(= 시점 T까지). revision 은 emit 당 1 증가하므로 slice(0,rev). */
23
+ until(revision) {
24
+ return this.events.slice(0, revision);
25
+ }
26
+ /** revision 이후(재동기·증분 전송용). */
27
+ since(revision) {
28
+ return this.events.slice(revision);
29
+ }
30
+ /** sim 시각(ISO eventTime) 이하 이벤트. */
31
+ untilSimTime(iso) {
32
+ return this.events.filter(e => e.eventTime <= iso);
33
+ }
34
+ }
35
+ /** 이벤트열 → 상태 재구성(시간여행). board = 마스터(토폴로지·무버). */
36
+ export function replay(board, events) {
37
+ const proj = new StateProjector(board);
38
+ for (const e of events)
39
+ proj.apply(e);
40
+ return proj.snapshot();
41
+ }
@@ -0,0 +1,42 @@
1
+ import type { CanonicalEnvelope } from './contract.ts';
2
+ import type { EpcisEvent } from './epcis.ts';
3
+ /** 매핑 값: 리터럴, 또는 소스 필드 참조("$.field.path"). */
4
+ export type MapValue = string;
5
+ /** ObjectEvent 매핑 스펙(선언적). Agg/Txn 매핑은 확장 지점. */
6
+ export interface ObjectEventMapping {
7
+ type: 'ObjectEvent';
8
+ action: MapValue;
9
+ bizStep: MapValue;
10
+ disposition?: MapValue;
11
+ epc: MapValue;
12
+ readPoint?: MapValue;
13
+ bizLocation?: MapValue;
14
+ }
15
+ export type EventMapping = ObjectEventMapping;
16
+ /** 룰: 소스 레코드의 판별자(sourceType)로 매핑을 고른다. */
17
+ export interface AdapterRule {
18
+ sourceType: string;
19
+ mapping: EventMapping;
20
+ }
21
+ export interface IngestOptions {
22
+ tenantId: string;
23
+ /** eventTime 소스 필드(없으면 defaultEventTime). */
24
+ eventTimePath?: string;
25
+ defaultEventTime: string;
26
+ }
27
+ export interface IngestResult {
28
+ accepted: CanonicalEnvelope[];
29
+ rejected: {
30
+ record: unknown;
31
+ errors: string[];
32
+ }[];
33
+ }
34
+ type Rec = Record<string, unknown>;
35
+ /** 단일 레코드 → 정규 EPCIS 이벤트(매핑만; 검증은 ingest 에서). */
36
+ export declare function mapRecord(record: Rec, mapping: EventMapping, eventTime: string): EpcisEvent;
37
+ /**
38
+ * 레거시 레코드 배열 → 정규 EPCIS 봉투. 매핑 후 검증, 실패분은 rejected(오염 차단).
39
+ * 실 EPCIS-native 소스면 매핑은 거의 항등, 검증만 통과시키면 됨.
40
+ */
41
+ export declare function ingest(records: Rec[], rules: AdapterRule[], opts: IngestOptions): IngestResult;
42
+ export {};
@@ -0,0 +1,69 @@
1
+ /*
2
+ * Face 2 — 레거시 ↔ 레퍼런스 어댑터 (anti-corruption layer, 모니터링 ingress).
3
+ * 설계 SoT: integration/face2-adapters.md
4
+ *
5
+ * 실 WMS 의 다양한 페이로드를 **선언적 매핑**으로 정규 EPCIS 로 변환 + **검증**.
6
+ * 여기서 `validateEpcisEvent` 가 실 소비처를 얻는다(검증 실패 = ACL 이 오염 차단).
7
+ *
8
+ * 매핑 엔진은 walking-skeleton 범위에서 무의존 declarative spec 사용.
9
+ * (jsonata/템플릿 엔진 최종 선택은 열린 결정 — face2-adapters.md. 커널 zero-dep 유지 위해 여기선 미도입.)
10
+ */
11
+ import { objectEvent, validateEpcisEvent } from "./epcis.js";
12
+ function get(obj, path) {
13
+ return path.split('.').reduce((o, k) => (o == null ? o : o[k]), obj);
14
+ }
15
+ /** 매핑 값 해석: "$.path" 는 소스 참조, 그 외는 리터럴. */
16
+ function resolve(v, record) {
17
+ if (v === undefined)
18
+ return undefined;
19
+ if (v.startsWith('$.')) {
20
+ const r = get(record, v.slice(2));
21
+ return r === undefined || r === null ? undefined : String(r);
22
+ }
23
+ return v;
24
+ }
25
+ /** 단일 레코드 → 정규 EPCIS 이벤트(매핑만; 검증은 ingest 에서). */
26
+ export function mapRecord(record, mapping, eventTime) {
27
+ const epc = resolve(mapping.epc, record);
28
+ return objectEvent({
29
+ eventTime,
30
+ action: (resolve(mapping.action, record) ?? ''),
31
+ bizStep: resolve(mapping.bizStep, record) ?? '',
32
+ disposition: resolve(mapping.disposition, record),
33
+ epcList: epc ? [epc] : [],
34
+ readPoint: resolve(mapping.readPoint, record),
35
+ bizLocation: resolve(mapping.bizLocation, record)
36
+ });
37
+ }
38
+ /**
39
+ * 레거시 레코드 배열 → 정규 EPCIS 봉투. 매핑 후 검증, 실패분은 rejected(오염 차단).
40
+ * 실 EPCIS-native 소스면 매핑은 거의 항등, 검증만 통과시키면 됨.
41
+ */
42
+ export function ingest(records, rules, opts) {
43
+ const ruleByType = new Map(rules.map(r => [r.sourceType, r]));
44
+ const accepted = [];
45
+ const rejected = [];
46
+ let seq = 0;
47
+ for (const record of records) {
48
+ const rule = ruleByType.get(String(record['sourceType']));
49
+ if (!rule) {
50
+ rejected.push({ record, errors: [`매칭 룰 없음: sourceType=${record['sourceType']}`] });
51
+ continue;
52
+ }
53
+ const eventTime = (opts.eventTimePath ? resolve('$.' + opts.eventTimePath, record) : undefined) ?? opts.defaultEventTime;
54
+ const ev = mapRecord(record, rule.mapping, eventTime);
55
+ const errors = validateEpcisEvent(ev);
56
+ if (errors.length) {
57
+ rejected.push({ record, errors });
58
+ continue;
59
+ }
60
+ accepted.push({
61
+ eventId: `${opts.tenantId}-ingest-${++seq}`,
62
+ eventType: `epcis.${ev.type}`,
63
+ eventTime: ev.eventTime,
64
+ tenantId: opts.tenantId,
65
+ data: ev
66
+ });
67
+ }
68
+ return { accepted, rejected };
69
+ }