@operato/twin-kernel 0.7.33 → 0.7.38
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.
- package/dist/contract.d.ts +13 -0
- package/dist/face2-adapter.d.ts +42 -3
- package/dist/face2-adapter.js +101 -21
- package/dist/flow-engine.js +5 -0
- package/dist/observed-reducer.d.ts +40 -1
- package/dist/observed-reducer.js +105 -7
- package/dist-cjs/index.cjs +156 -25
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -1397,6 +1397,19 @@ export interface StateSnapshot {
|
|
|
1397
1397
|
tasks: TaskState[];
|
|
1398
1398
|
orders: OrderState[];
|
|
1399
1399
|
attentions?: Attention[];
|
|
1400
|
+
/**
|
|
1401
|
+
* **담을 줄 몰라 반영하지 못한 사실** — 종류별 수(관측 구동에서만 나온다).
|
|
1402
|
+
*
|
|
1403
|
+
* 관측 리듀서는 오래전부터 이것을 세어 왔다. 그런데 **스냅샷이 그 값을 떨어뜨렸다** — 그래서
|
|
1404
|
+
* 이미 그것을 읽도록 쓰여 있던 정합성 검사(`twin-unhandled-vocabulary`)는 미러에서 영원히
|
|
1405
|
+
* 조용했다(2026-08-19 실측). 세기만 하고 아무도 못 보는 값은 없는 것과 같다.
|
|
1406
|
+
*/
|
|
1407
|
+
unhandled?: {
|
|
1408
|
+
eventType: string;
|
|
1409
|
+
count: number;
|
|
1410
|
+
firstAtMs?: number;
|
|
1411
|
+
lastAtMs?: number;
|
|
1412
|
+
}[];
|
|
1400
1413
|
/**
|
|
1401
1414
|
* 확인(ack)해 둔 주목 신호 id — **상태에서 파생되지 않는 유일한 축.**
|
|
1402
1415
|
*
|
package/dist/face2-adapter.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { CanonicalEnvelope } from './contract.ts';
|
|
|
2
2
|
import type { EpcisEvent } from './epcis.ts';
|
|
3
3
|
/** 매핑 값: 리터럴, 또는 소스 필드 참조("$.field.path"). */
|
|
4
4
|
export type MapValue = string;
|
|
5
|
-
/** ObjectEvent 매핑 스펙(선언적).
|
|
5
|
+
/** ObjectEvent 매핑 스펙(선언적). */
|
|
6
6
|
export interface ObjectEventMapping {
|
|
7
7
|
type: 'ObjectEvent';
|
|
8
8
|
action: MapValue;
|
|
@@ -12,7 +12,41 @@ export interface ObjectEventMapping {
|
|
|
12
12
|
readPoint?: MapValue;
|
|
13
13
|
bizLocation?: MapValue;
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* AggregationEvent 매핑 스펙 — **담김의 사실**(무엇이 무엇에 실렸나).
|
|
17
|
+
*
|
|
18
|
+
* ── 왜 뒤늦게 생겼나 (2026-08-19) ───────────────────────────────────────────
|
|
19
|
+
* 이 파일은 오래 *"Agg/Txn 매핑은 확장 지점"* 이라고만 적어 두었고, 그래서 원본이 **적재를 말할 길이
|
|
20
|
+
* 없었다.** 관측 리듀서는 `AggregationEvent` 를 받으면 자식 물품에 `parent` 를 붙이도록 이미 되어 있었는데
|
|
21
|
+
* (3D 가 「이 상자가 어느 팔레트에 실렸나」를 묻는 자리) 그 사실이 문 앞에서 사라졌다 — 시뮬 원본을
|
|
22
|
+
* 붙여 돌려 보고서야 그것이 로그로 드러났다(*"is not carried to the twin"*).
|
|
23
|
+
*
|
|
24
|
+
* 자식은 **두 가지로 말할 수 있다**(표준이 그렇고 우리 커널도 둘 다 낸다):
|
|
25
|
+
* · `childEPCs` — 개체 하나하나(상자 3개의 식별자).
|
|
26
|
+
* · `childQuantityList` — 클래스와 수량(「이 품번 40개」). 낱개 식별자가 없는 입고·포장이 이 모양이다.
|
|
27
|
+
* 하나만 받으면 나머지 사실이 문 앞에서 사라진다 — 실제로 그렇게 사라지고 있었다(로그가 짚었다).
|
|
28
|
+
*/
|
|
29
|
+
export interface AggregationEventMapping {
|
|
30
|
+
type: 'AggregationEvent';
|
|
31
|
+
action: MapValue;
|
|
32
|
+
bizStep: MapValue;
|
|
33
|
+
disposition?: MapValue;
|
|
34
|
+
parentID: MapValue;
|
|
35
|
+
/** 자식 목록 참조("$.childEPCs") — 문자열 배열이어야 한다. */
|
|
36
|
+
childEPCs?: MapValue;
|
|
37
|
+
/** 수량으로 담긴 자식 참조("$.childQuantityList") — `{ epcClass, quantity, uom? }` 배열. */
|
|
38
|
+
childQuantityList?: MapValue;
|
|
39
|
+
readPoint?: MapValue;
|
|
40
|
+
bizLocation?: MapValue;
|
|
41
|
+
}
|
|
42
|
+
export type EventMapping = ObjectEventMapping | AggregationEventMapping;
|
|
43
|
+
/**
|
|
44
|
+
* 이 품목 레코드가 **담김의 사실**인가 — 라우팅 판정을 한 곳에 둔다(소비처가 각자 짐작하지 않게).
|
|
45
|
+
*
|
|
46
|
+
* 담김은 부모와 자식으로 말한다(`parentID`·`childEPCs`). 개체 하나의 관측(`epc`)과 섞이지 않게, 부모가
|
|
47
|
+
* 있으면 담김으로 본다 — 둘 다 있으면 커넥터가 무엇을 말하려는지 알 수 없으므로 담김으로 받지 않는다.
|
|
48
|
+
*/
|
|
49
|
+
export declare function isAggregationRecord(record: unknown): boolean;
|
|
16
50
|
/** 룰: 소스 레코드의 판별자(sourceType)로 매핑을 고른다. */
|
|
17
51
|
export interface AdapterRule {
|
|
18
52
|
sourceType: string;
|
|
@@ -32,7 +66,12 @@ export interface IngestResult {
|
|
|
32
66
|
}[];
|
|
33
67
|
}
|
|
34
68
|
type Rec = Record<string, unknown>;
|
|
35
|
-
/** 단일 레코드 → 정규 EPCIS 이벤트(
|
|
69
|
+
/** 단일 레코드 → 정규 EPCIS 이벤트 + 매핑에서 드러난 문제(검증은 `ingest` 가 이어서 한다). */
|
|
70
|
+
export declare function mapRecordChecked(record: Rec, mapping: EventMapping, eventTime: string): {
|
|
71
|
+
event: EpcisEvent;
|
|
72
|
+
errors: string[];
|
|
73
|
+
};
|
|
74
|
+
/** 단일 레코드 → 정규 EPCIS 이벤트(매핑만). 매핑 문제까지 보려면 `mapRecordChecked`. */
|
|
36
75
|
export declare function mapRecord(record: Rec, mapping: EventMapping, eventTime: string): EpcisEvent;
|
|
37
76
|
/**
|
|
38
77
|
* 레거시 레코드 배열 → 정규 EPCIS 봉투. 매핑 후 검증, 실패분은 rejected(오염 차단).
|
package/dist/face2-adapter.js
CHANGED
|
@@ -8,32 +8,110 @@
|
|
|
8
8
|
* 매핑 엔진은 walking-skeleton 범위에서 무의존 declarative spec 사용.
|
|
9
9
|
* (jsonata/템플릿 엔진 최종 선택은 열린 결정 — face2-adapters.md. 커널 zero-dep 유지 위해 여기선 미도입.)
|
|
10
10
|
*/
|
|
11
|
-
import { objectEvent, validateEpcisEvent } from "./epcis.js";
|
|
11
|
+
import { aggregationEvent, objectEvent, validateEpcisEvent } from "./epcis.js";
|
|
12
|
+
/**
|
|
13
|
+
* 이 품목 레코드가 **담김의 사실**인가 — 라우팅 판정을 한 곳에 둔다(소비처가 각자 짐작하지 않게).
|
|
14
|
+
*
|
|
15
|
+
* 담김은 부모와 자식으로 말한다(`parentID`·`childEPCs`). 개체 하나의 관측(`epc`)과 섞이지 않게, 부모가
|
|
16
|
+
* 있으면 담김으로 본다 — 둘 다 있으면 커넥터가 무엇을 말하려는지 알 수 없으므로 담김으로 받지 않는다.
|
|
17
|
+
*/
|
|
18
|
+
export function isAggregationRecord(record) {
|
|
19
|
+
if (!record || typeof record !== 'object')
|
|
20
|
+
return false;
|
|
21
|
+
const r = record;
|
|
22
|
+
return typeof r.parentID === 'string' && r.parentID.trim().length > 0 && r.epc === undefined;
|
|
23
|
+
}
|
|
12
24
|
function get(obj, path) {
|
|
13
25
|
return path.split('.').reduce((o, k) => (o == null ? o : o[k]), obj);
|
|
14
26
|
}
|
|
15
|
-
/**
|
|
16
|
-
|
|
27
|
+
/**
|
|
28
|
+
* 매핑 값 해석: "$.path" 는 소스 참조, 그 외는 리터럴.
|
|
29
|
+
*
|
|
30
|
+
* ── 객체를 문자열로 굳히지 않는다 (2026-08-19 실측으로 고침) ────────────────
|
|
31
|
+
* 예전에는 무엇이 오든 `String(값)` 이었다. 그래서 커널이 낸 EPCIS 의 `readPoint`(= `{ id }`)를 이 문에
|
|
32
|
+
* 그대로 실은 커넥터가 있었고, **오류 하나 없이** `readPoint: { id: "[object Object]" }` 가 저널에 쌓였다
|
|
33
|
+
* (실 앱에서 관측). 자리를 잃은 관측은 「어디 있나」에 답하지 못하는데 화면은 값이 있다고 믿는다.
|
|
34
|
+
*
|
|
35
|
+
* 그러니 객체·배열은 **매핑 오류**다. 조용히 버리지도(그러면 자리가 없어진 이유를 아무도 모른다),
|
|
36
|
+
* 문자열로 굳히지도 않고, 그 레코드를 이유와 함께 거부한다.
|
|
37
|
+
*/
|
|
38
|
+
function resolve(v, record, field, errors) {
|
|
17
39
|
if (v === undefined)
|
|
18
40
|
return undefined;
|
|
19
|
-
if (v.startsWith('$.'))
|
|
20
|
-
|
|
21
|
-
|
|
41
|
+
if (!v.startsWith('$.'))
|
|
42
|
+
return v;
|
|
43
|
+
const r = get(record, v.slice(2));
|
|
44
|
+
if (r === undefined || r === null)
|
|
45
|
+
return undefined;
|
|
46
|
+
if (typeof r === 'object') {
|
|
47
|
+
errors.push(`${field} 가 값이 아니라 ${Array.isArray(r) ? '배열' : '객체'}로 왔다(${v}) — 커넥터가 표준 구조를 그대로 실었다: ${JSON.stringify(r).slice(0, 80)}`);
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
return String(r);
|
|
51
|
+
}
|
|
52
|
+
/** 목록 참조 해석 — 문자열 배열만 받는다(하나만 온 것을 배열로 지어내지 않는다). */
|
|
53
|
+
function resolveList(v, record, field, errors) {
|
|
54
|
+
if (v === undefined)
|
|
55
|
+
return [];
|
|
56
|
+
const r = get(record, v.startsWith('$.') ? v.slice(2) : v);
|
|
57
|
+
if (r === undefined || r === null)
|
|
58
|
+
return [];
|
|
59
|
+
if (!Array.isArray(r) || r.some(x => typeof x !== 'string')) {
|
|
60
|
+
errors.push(`${field} 가 문자열 배열이 아니다(${v}): ${JSON.stringify(r).slice(0, 80)}`);
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
return r.slice();
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 수량 목록 참조 해석 — `{ epcClass, quantity, uom? }` 배열.
|
|
67
|
+
*
|
|
68
|
+
* 값의 옳고 그름(클래스 식별자인가·수량이 양수인가)은 **검증이 본다**(`validateEpcisEvent`) — 여기서
|
|
69
|
+
* 다시 판정하면 규칙이 두 벌이 된다. 이 자리는 「배열인가·객체인가」만 지킨다.
|
|
70
|
+
*/
|
|
71
|
+
function resolveQuantityList(v, record, field, errors) {
|
|
72
|
+
if (v === undefined)
|
|
73
|
+
return [];
|
|
74
|
+
const r = get(record, v.startsWith('$.') ? v.slice(2) : v);
|
|
75
|
+
if (r === undefined || r === null)
|
|
76
|
+
return [];
|
|
77
|
+
if (!Array.isArray(r) || r.some(x => !x || typeof x !== 'object' || Array.isArray(x))) {
|
|
78
|
+
errors.push(`${field} 가 객체 배열이 아니다(${v}): ${JSON.stringify(r).slice(0, 80)}`);
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
return r.map(x => ({ ...x }));
|
|
82
|
+
}
|
|
83
|
+
/** 단일 레코드 → 정규 EPCIS 이벤트 + 매핑에서 드러난 문제(검증은 `ingest` 가 이어서 한다). */
|
|
84
|
+
export function mapRecordChecked(record, mapping, eventTime) {
|
|
85
|
+
const errors = [];
|
|
86
|
+
const action = (resolve(mapping.action, record, 'action', errors) ?? '');
|
|
87
|
+
const bizStep = resolve(mapping.bizStep, record, 'bizStep', errors) ?? '';
|
|
88
|
+
const disposition = resolve(mapping.disposition, record, 'disposition', errors);
|
|
89
|
+
const readPoint = resolve(mapping.readPoint, record, 'readPoint', errors);
|
|
90
|
+
const bizLocation = resolve(mapping.bizLocation, record, 'bizLocation', errors);
|
|
91
|
+
if (mapping.type === 'AggregationEvent') {
|
|
92
|
+
const parentID = resolve(mapping.parentID, record, 'parentID', errors) ?? '';
|
|
93
|
+
const childEPCs = resolveList(mapping.childEPCs, record, 'childEPCs', errors);
|
|
94
|
+
const childQuantityList = resolveQuantityList(mapping.childQuantityList, record, 'childQuantityList', errors);
|
|
95
|
+
return {
|
|
96
|
+
event: aggregationEvent({
|
|
97
|
+
eventTime, action, bizStep, disposition, parentID,
|
|
98
|
+
/* 없는 쪽은 **필드를 만들지 않는다** — 빈 배열을 실으면 「자식이 없다」고 말하는 것이 된다. */
|
|
99
|
+
...(childEPCs.length ? { childEPCs } : {}),
|
|
100
|
+
...(childQuantityList.length ? { childQuantityList } : {}),
|
|
101
|
+
readPoint, bizLocation
|
|
102
|
+
}),
|
|
103
|
+
errors
|
|
104
|
+
};
|
|
22
105
|
}
|
|
23
|
-
|
|
106
|
+
const epc = resolve(mapping.epc, record, 'epc', errors);
|
|
107
|
+
return {
|
|
108
|
+
event: objectEvent({ eventTime, action, bizStep, disposition, epcList: epc ? [epc] : [], readPoint, bizLocation }),
|
|
109
|
+
errors
|
|
110
|
+
};
|
|
24
111
|
}
|
|
25
|
-
/** 단일 레코드 → 정규 EPCIS 이벤트(
|
|
112
|
+
/** 단일 레코드 → 정규 EPCIS 이벤트(매핑만). 매핑 문제까지 보려면 `mapRecordChecked`. */
|
|
26
113
|
export function mapRecord(record, mapping, eventTime) {
|
|
27
|
-
|
|
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
|
-
});
|
|
114
|
+
return mapRecordChecked(record, mapping, eventTime).event;
|
|
37
115
|
}
|
|
38
116
|
/**
|
|
39
117
|
* 레거시 레코드 배열 → 정규 EPCIS 봉투. 매핑 후 검증, 실패분은 rejected(오염 차단).
|
|
@@ -50,9 +128,11 @@ export function ingest(records, rules, opts) {
|
|
|
50
128
|
rejected.push({ record, errors: [`매칭 룰 없음: sourceType=${record['sourceType']}`] });
|
|
51
129
|
continue;
|
|
52
130
|
}
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
const errors =
|
|
131
|
+
const timeErrors = [];
|
|
132
|
+
const eventTime = (opts.eventTimePath ? resolve('$.' + opts.eventTimePath, record, 'eventTime', timeErrors) : undefined) ?? opts.defaultEventTime;
|
|
133
|
+
const { event: ev, errors: mapErrors } = mapRecordChecked(record, rule.mapping, eventTime);
|
|
134
|
+
/* 매핑에서 드러난 문제를 검증 위반과 **같은 자리**에 담는다 — 부르는 쪽은 이유를 한 곳에서 읽는다. */
|
|
135
|
+
const errors = [...timeErrors, ...mapErrors, ...validateEpcisEvent(ev)];
|
|
56
136
|
if (errors.length) {
|
|
57
137
|
rejected.push({ record, errors });
|
|
58
138
|
continue;
|
package/dist/flow-engine.js
CHANGED
|
@@ -1086,6 +1086,11 @@ export class FlowEngine {
|
|
|
1086
1086
|
/* 스냅샷이 **델타보다 가난하면 안 된다** — 예전에는 소요·남은 시간을 빼고 내보내서, 이 스냅샷으로
|
|
1087
1087
|
* 다른 커널을 주입하면(hydrateObserved) 진행 중이던 작업을 이어 굴릴 수 없었다(미러 스냅샷은
|
|
1088
1088
|
* 델타에서 왔으므로 갖고 있었다 — 같은 계약을 두 구동이 다르게 채우던 자리). */
|
|
1089
|
+
/* 관측이 담지 못한 사실을 그대로 내보낸다 — 리듀서가 세고 있어도 여기서 떨어뜨리면 아무도 못 본다. */
|
|
1090
|
+
...(() => {
|
|
1091
|
+
const u = this.observer?.snapshot?.().unhandled;
|
|
1092
|
+
return u?.length ? { unhandled: u } : {};
|
|
1093
|
+
})(),
|
|
1089
1094
|
tasks: [...this.tasks.values()].map(t => ({
|
|
1090
1095
|
id: t.id, kind: t.kind, status: t.status, itemRefs: [t.itemEpc],
|
|
1091
1096
|
fromNode: t.fromNode, toNode: t.toNode, resourceRef: t.resource ?? undefined, orderId: t.orderId,
|
|
@@ -106,6 +106,23 @@ export interface ReducerCheckpoint {
|
|
|
106
106
|
child: string;
|
|
107
107
|
parent: string;
|
|
108
108
|
}[];
|
|
109
|
+
/** 수량으로 담긴 내용 — 채워 넣은 것과, 팔레트를 아직 못 본 채 보류한 것. */
|
|
110
|
+
qtyAggregation?: {
|
|
111
|
+
parent: string;
|
|
112
|
+
quantities: {
|
|
113
|
+
epcClass?: string;
|
|
114
|
+
quantity?: number;
|
|
115
|
+
uom?: string;
|
|
116
|
+
}[];
|
|
117
|
+
}[];
|
|
118
|
+
pendingQuantities?: {
|
|
119
|
+
parent: string;
|
|
120
|
+
quantities: {
|
|
121
|
+
epcClass?: string;
|
|
122
|
+
quantity?: number;
|
|
123
|
+
uom?: string;
|
|
124
|
+
}[];
|
|
125
|
+
}[];
|
|
109
126
|
tasks: TaskState[];
|
|
110
127
|
equipment: EquipmentState[];
|
|
111
128
|
persons: PersonState[];
|
|
@@ -132,6 +149,15 @@ export declare class ObservedReducer {
|
|
|
132
149
|
private aggregation;
|
|
133
150
|
/** 아직 관측되지 않은 자식의 담김 — 물품을 지어내지 않고 보류했다가 등장할 때 붙인다. */
|
|
134
151
|
private pendingParent;
|
|
152
|
+
/**
|
|
153
|
+
* 팔레트가 **담고 있는 것**(클래스 + 수량) — 바코드 없이 수량만 실린 담김.
|
|
154
|
+
*
|
|
155
|
+
* · `qtyAggregation` — 우리가 그 팔레트 물품에 채워 넣은 내용(풀릴 때 되돌릴 근거).
|
|
156
|
+
* · `pendingQuantities` — 담김이 먼저 오고 그 팔레트를 아직 관측하지 못한 경우. 물품을 지어내지
|
|
157
|
+
* 않고 보류해 두었다가, 관측되는 순간 붙인다(`pendingParent` 와 같은 규율).
|
|
158
|
+
*/
|
|
159
|
+
private qtyAggregation;
|
|
160
|
+
private pendingQuantities;
|
|
135
161
|
private tasks;
|
|
136
162
|
private equipment;
|
|
137
163
|
private persons;
|
|
@@ -217,8 +243,21 @@ export declare class ObservedReducer {
|
|
|
217
243
|
private observedAtMs?;
|
|
218
244
|
/** 이벤트 1건 반영 — eventType 으로 EPCIS vs 운영 델타 분기. */
|
|
219
245
|
apply(e: CanonicalEnvelope): void;
|
|
220
|
-
/**
|
|
246
|
+
/**
|
|
247
|
+
* 반영하지 못한 사건을 종류별로 센다 — 처음·마지막 시각을 함께 남겨 「언제부터」에 답한다.
|
|
248
|
+
*
|
|
249
|
+
* `key` 를 주면 그 이름으로 센다: 종류 전체를 못 다룬 것과 **그 종류의 어떤 모양만** 못 다룬 것은
|
|
250
|
+
* 다른 사실이다(예: 담김은 반영하는데 품번이 섞인 팔레트만 못 담는다).
|
|
251
|
+
*/
|
|
221
252
|
private noteUnhandled;
|
|
253
|
+
/**
|
|
254
|
+
* 팔레트 물품에 **담고 있는 것**을 채운다 — 이미 아는 값을 덮지 않는다.
|
|
255
|
+
*
|
|
256
|
+
* 팔레트가 자기 품번·수량을 이미 들고 있으면(관측이 그렇게 말했으면) 담김 선언이 그것을 밀어내지
|
|
257
|
+
* 않는다. 비어 있던 자리만 채운다 — 「모른다」를 채우는 것이 이 작업의 목적이고, 아는 것을 바꾸는
|
|
258
|
+
* 것은 아니다.
|
|
259
|
+
*/
|
|
260
|
+
private withContainedQuantity;
|
|
222
261
|
private applyEpcis;
|
|
223
262
|
/**
|
|
224
263
|
* 물품 한 건 병합 — **아는 것을 잃지 않는다.** 새로 온 값이 우선, 없으면 기존 값 유지.
|
package/dist/observed-reducer.js
CHANGED
|
@@ -45,6 +45,15 @@ export class ObservedReducer {
|
|
|
45
45
|
aggregation = new Map();
|
|
46
46
|
/** 아직 관측되지 않은 자식의 담김 — 물품을 지어내지 않고 보류했다가 등장할 때 붙인다. */
|
|
47
47
|
pendingParent = new Map(); // 자식 EPC → 부모(물류단위)
|
|
48
|
+
/**
|
|
49
|
+
* 팔레트가 **담고 있는 것**(클래스 + 수량) — 바코드 없이 수량만 실린 담김.
|
|
50
|
+
*
|
|
51
|
+
* · `qtyAggregation` — 우리가 그 팔레트 물품에 채워 넣은 내용(풀릴 때 되돌릴 근거).
|
|
52
|
+
* · `pendingQuantities` — 담김이 먼저 오고 그 팔레트를 아직 관측하지 못한 경우. 물품을 지어내지
|
|
53
|
+
* 않고 보류해 두었다가, 관측되는 순간 붙인다(`pendingParent` 와 같은 규율).
|
|
54
|
+
*/
|
|
55
|
+
qtyAggregation = new Map();
|
|
56
|
+
pendingQuantities = new Map();
|
|
48
57
|
tasks = new Map();
|
|
49
58
|
equipment = new Map();
|
|
50
59
|
persons = new Map();
|
|
@@ -371,10 +380,16 @@ export class ObservedReducer {
|
|
|
371
380
|
this.noteUnhandled(e);
|
|
372
381
|
}
|
|
373
382
|
}
|
|
374
|
-
/**
|
|
375
|
-
|
|
383
|
+
/**
|
|
384
|
+
* 반영하지 못한 사건을 종류별로 센다 — 처음·마지막 시각을 함께 남겨 「언제부터」에 답한다.
|
|
385
|
+
*
|
|
386
|
+
* `key` 를 주면 그 이름으로 센다: 종류 전체를 못 다룬 것과 **그 종류의 어떤 모양만** 못 다룬 것은
|
|
387
|
+
* 다른 사실이다(예: 담김은 반영하는데 품번이 섞인 팔레트만 못 담는다).
|
|
388
|
+
*/
|
|
389
|
+
noteUnhandled(e, key) {
|
|
376
390
|
const at = Date.parse(String(e.eventTime ?? ''));
|
|
377
|
-
const
|
|
391
|
+
const name = key ?? e.eventType;
|
|
392
|
+
const cur = this.unhandled.get(name) ?? { count: 0 };
|
|
378
393
|
cur.count++;
|
|
379
394
|
if (Number.isFinite(at)) {
|
|
380
395
|
if (cur.firstAtMs === undefined || at < cur.firstAtMs)
|
|
@@ -382,7 +397,29 @@ export class ObservedReducer {
|
|
|
382
397
|
if (cur.lastAtMs === undefined || at > cur.lastAtMs)
|
|
383
398
|
cur.lastAtMs = at;
|
|
384
399
|
}
|
|
385
|
-
this.unhandled.set(
|
|
400
|
+
this.unhandled.set(name, cur);
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* 팔레트 물품에 **담고 있는 것**을 채운다 — 이미 아는 값을 덮지 않는다.
|
|
404
|
+
*
|
|
405
|
+
* 팔레트가 자기 품번·수량을 이미 들고 있으면(관측이 그렇게 말했으면) 담김 선언이 그것을 밀어내지
|
|
406
|
+
* 않는다. 비어 있던 자리만 채운다 — 「모른다」를 채우는 것이 이 작업의 목적이고, 아는 것을 바꾸는
|
|
407
|
+
* 것은 아니다.
|
|
408
|
+
*/
|
|
409
|
+
withContainedQuantity(cur, q, all) {
|
|
410
|
+
const parsed = q.epcClass ? parseEpc(q.epcClass) : undefined;
|
|
411
|
+
const declared = all.filter(x => typeof x.quantity === 'number');
|
|
412
|
+
return {
|
|
413
|
+
...cur,
|
|
414
|
+
gtin: cur.gtin ?? q.epcClass,
|
|
415
|
+
gtinKey: cur.gtinKey ?? parsed?.gtinKey,
|
|
416
|
+
/* 관측이 준 수량이 있으면 그대로 둔다. 없으면(팔레트 하나로 세어 `1` 이던 자리) 내용의 수량이 답이다. */
|
|
417
|
+
qty: cur.qty !== undefined && cur.qty !== 1 ? cur.qty : q.quantity,
|
|
418
|
+
uom: cur.uom ?? q.uom,
|
|
419
|
+
/* 단위가 여럿일 때만 목록을 든다(하나면 `qty`/`uom` 이 무손실로 들고 있다 — 위 `upsert` 와 같은 규율). */
|
|
420
|
+
quantities: cur.quantities ?? (declared.length >= 2 ? declared.map(x => ({ value: x.quantity, ...(x.uom ? { uom: x.uom } : {}) })) : undefined),
|
|
421
|
+
lot: cur.lot ?? parsed?.lot
|
|
422
|
+
};
|
|
386
423
|
}
|
|
387
424
|
applyEpcis(ev, envelope) {
|
|
388
425
|
/* 정정 선언이 붙은 이벤트는 **새 사실이 아니다** — 앞선 이벤트를 취소·수정하는 선언이다.
|
|
@@ -399,6 +436,46 @@ export class ObservedReducer {
|
|
|
399
436
|
return;
|
|
400
437
|
}
|
|
401
438
|
if (ev.type === 'AggregationEvent') {
|
|
439
|
+
/*
|
|
440
|
+
* ── 바코드 없이 **수량만** 실린 담김 (2026-08-19) ────────────────────────
|
|
441
|
+
* 실제 창고는 박스마다 바코드를 붙이지 않는다. 팔레트 하나에 「이 품번 40개」로 입고하는 것이
|
|
442
|
+
* 훨씬 흔하고, 표준도 그 자리를 둔다(`childQuantityList`: 클래스 + 수량).
|
|
443
|
+
*
|
|
444
|
+
* 그런데 여기는 **낱개 자식만** 반영했다. 그래서 미러는 그 팔레트를 `{epc, location, qty: 1}` 로
|
|
445
|
+
* 들었다 — 실측: 품번도 없고 40개도 없다. 화면에는 「팔레트 1개」만 보이고 그 안의 40개는 재고
|
|
446
|
+
* 집계에서 통째로 빠진다. 실 WMS 를 붙이면 바로 부딪히는 자리다.
|
|
447
|
+
*
|
|
448
|
+
* **어떻게 담나**: 시뮬 커널이 같은 입고를 다루는 방식과 맞춘다 — 그쪽은 팔레트 물품 자신에
|
|
449
|
+
* 품번과 수량을 싣는다(`items.set(epc, { epc, gtin, qty … })`). 그래서 여기서도 팔레트 물품에
|
|
450
|
+
* 채운다. 새 필드를 만들지 않으므로 **기존 재고 집계가 그대로 센다**(필드만 만들고 아무도 읽지
|
|
451
|
+
* 않는 상태를 이 프로젝트는 결함으로 본다). 두 구동이 같은 모양을 들게 되는 것이 덤이다.
|
|
452
|
+
*
|
|
453
|
+
* **못 하는 경우는 밝힌다**: 한 팔레트에 품번이 둘 이상이면 이 모양으로 표현할 수 없다(하나를
|
|
454
|
+
* 골라 채우면 나머지를 지우는 것이다). 그때는 채우지 않고 `unhandled` 로 센다 — 조용히 버리지
|
|
455
|
+
* 않는다. 그 어휘를 담으려면 상태에 「담고 있는 것들」이 따로 필요하고, 그것은 별 결정이다.
|
|
456
|
+
*/
|
|
457
|
+
if (ev.action === 'ADD' && ev.childQuantityList?.length) {
|
|
458
|
+
const classes = new Set(ev.childQuantityList.map(q => q.epcClass));
|
|
459
|
+
if (classes.size > 1) {
|
|
460
|
+
/* 여러 품번이 한 팔레트에 — 지금 상태로는 표현할 수 없다. 세어서 드러낸다. */
|
|
461
|
+
if (envelope)
|
|
462
|
+
this.noteUnhandled(envelope, 'aggregation-mixed-classes');
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
const q = ev.childQuantityList[0];
|
|
466
|
+
const cur = this.items.get(ev.parentID);
|
|
467
|
+
/*
|
|
468
|
+
* **관측하지 못한 팔레트를 지어내지 않는다** — 담김은 *어디 있는지*를 말하지 않는다.
|
|
469
|
+
* 그 팔레트가 관측되면(ObjectEvent) 그때 위치와 함께 서고, 담김은 보류해 두었다가 붙인다.
|
|
470
|
+
*/
|
|
471
|
+
if (!cur)
|
|
472
|
+
this.pendingQuantities.set(ev.parentID, [...ev.childQuantityList]);
|
|
473
|
+
else {
|
|
474
|
+
this.items.set(cur.subLotId ?? cur.epc, this.withContainedQuantity(cur, q, ev.childQuantityList));
|
|
475
|
+
this.qtyAggregation.set(ev.parentID, [...ev.childQuantityList]);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
402
479
|
if (ev.action === 'ADD' && ev.childEPCs?.length) {
|
|
403
480
|
this.aggregation.set(ev.parentID, [...ev.childEPCs]);
|
|
404
481
|
/* 조립 관계를 물품에도 주입한다 — 예전에는 내부 맵에만 두고 밖으로 내보내지 않아, 3D 가
|
|
@@ -424,6 +501,16 @@ export class ObservedReducer {
|
|
|
424
501
|
this.pendingParent.delete(child);
|
|
425
502
|
}
|
|
426
503
|
this.aggregation.delete(ev.parentID);
|
|
504
|
+
/* 수량으로 담겼던 것을 풀면 그 팔레트는 **비게 된다** — 우리가 채운 것만 되돌린다(원래
|
|
505
|
+
팔레트 자신의 수량이었다면 건드리지 않는다). */
|
|
506
|
+
if (this.qtyAggregation.delete(ev.parentID)) {
|
|
507
|
+
const cur = this.items.get(ev.parentID);
|
|
508
|
+
if (cur) {
|
|
509
|
+
const { gtin, gtinKey, qty, uom, quantities, lot, ...rest } = cur;
|
|
510
|
+
this.items.set(cur.subLotId ?? cur.epc, { ...rest, qty: 1 });
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
this.pendingQuantities.delete(ev.parentID);
|
|
427
514
|
}
|
|
428
515
|
return;
|
|
429
516
|
}
|
|
@@ -512,7 +599,9 @@ export class ObservedReducer {
|
|
|
512
599
|
const parsedSelf = parseEpc(epc);
|
|
513
600
|
/* `gtin` 은 **클래스 URI 원문**이다(오더 매칭이 이 값을 쓴다 — 뜻을 바꾸면 조용히 안 맞는다).
|
|
514
601
|
* 파서로 뜯은 품번 키·로트는 **별도 필드**로 얹는다. */
|
|
515
|
-
|
|
602
|
+
/* 담김이 먼저 온 팔레트는 그 내용의 클래스가 곧 이 물품의 품번이다(시뮬이 같은 입고를 그렇게 든다). */
|
|
603
|
+
const pendingQty = this.pendingQuantities.get(epc);
|
|
604
|
+
const classUri = q?.epcClass ?? pendingQty?.[0]?.epcClass ?? (parsedSelf.instance ? undefined : epc);
|
|
516
605
|
return {
|
|
517
606
|
epc,
|
|
518
607
|
...(subLotId ? { subLotId } : {}),
|
|
@@ -521,8 +610,12 @@ export class ObservedReducer {
|
|
|
521
610
|
location: patch.location ?? cur?.location ?? '',
|
|
522
611
|
disposition: patch.disposition ?? cur?.disposition,
|
|
523
612
|
parent: cur?.parent ?? this.pendingParent.get(epc),
|
|
524
|
-
|
|
525
|
-
|
|
613
|
+
/*
|
|
614
|
+
* 관측이 수량을 말하지 않았는데 **담김이 먼저 와 있었다면** 그 내용이 답이다(바코드 없이 수량만
|
|
615
|
+
* 실린 팔레트). 보류해 둔 것을 여기서 붙인다 — 위 `parent` 와 같은 규율이다.
|
|
616
|
+
*/
|
|
617
|
+
qty: q?.quantity ?? this.pendingQuantities.get(epc)?.[0]?.quantity ?? cur?.qty,
|
|
618
|
+
uom: q?.uom ?? this.pendingQuantities.get(epc)?.[0]?.uom ?? cur?.uom,
|
|
526
619
|
/*
|
|
527
620
|
* 선언된 수량 전부 — 값이 없는 항목은 담지 않는다(모름을 0 으로 만들지 않는다).
|
|
528
621
|
*
|
|
@@ -656,6 +749,9 @@ export class ObservedReducer {
|
|
|
656
749
|
items: [...this.items.values()].map(i => ({ ...i })),
|
|
657
750
|
aggregation: [...this.aggregation.entries()].map(([parent, children]) => ({ parent, children: [...children] })),
|
|
658
751
|
pendingParent: [...this.pendingParent.entries()].map(([child, parent]) => ({ child, parent })),
|
|
752
|
+
/* 수량으로 담긴 것도 이어받는다 — 되풀어 주지 않는 축이다(원천은 담김을 다시 말해 주지 않는다). */
|
|
753
|
+
qtyAggregation: [...this.qtyAggregation.entries()].map(([parent, quantities]) => ({ parent, quantities })),
|
|
754
|
+
pendingQuantities: [...this.pendingQuantities.entries()].map(([parent, quantities]) => ({ parent, quantities })),
|
|
659
755
|
tasks: [...this.tasks.values()].map(t => ({ ...t })),
|
|
660
756
|
equipment: [...this.equipment.values()].map(m => ({ ...m })),
|
|
661
757
|
persons: [...this.persons.values()].map(x => ({ ...x })),
|
|
@@ -677,6 +773,8 @@ export class ObservedReducer {
|
|
|
677
773
|
this.items = new Map((cp?.items ?? []).map(i => [i.epc, { ...i }]));
|
|
678
774
|
this.aggregation = new Map((cp?.aggregation ?? []).map(a => [a.parent, [...a.children]]));
|
|
679
775
|
this.pendingParent = new Map((cp?.pendingParent ?? []).map(x => [x.child, x.parent]));
|
|
776
|
+
this.qtyAggregation = new Map((cp?.qtyAggregation ?? []).map(x => [x.parent, x.quantities]));
|
|
777
|
+
this.pendingQuantities = new Map((cp?.pendingQuantities ?? []).map(x => [x.parent, x.quantities]));
|
|
680
778
|
this.tasks = new Map((cp?.tasks ?? []).map(t => [t.id, { ...t }]));
|
|
681
779
|
this.equipment = new Map((cp?.equipment ?? []).map(m => [m.id, { ...m }]));
|
|
682
780
|
this.persons = new Map((cp?.persons ?? []).map(x => [x.id, { ...x }]));
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -118,6 +118,7 @@ __export(index_exports, {
|
|
|
118
118
|
ingestEnergyEquipmentRecords: () => ingestEnergyEquipmentRecords,
|
|
119
119
|
ingestEnergyRecords: () => ingestEnergyRecords,
|
|
120
120
|
ingestOperationalRecords: () => ingestOperationalRecords,
|
|
121
|
+
isAggregationRecord: () => isAggregationRecord,
|
|
121
122
|
isElectricalLocationType: () => isElectricalLocationType,
|
|
122
123
|
isEnergyEquipmentRecord: () => isEnergyEquipmentRecord,
|
|
123
124
|
isEnergyRecord: () => isEnergyRecord,
|
|
@@ -129,6 +130,7 @@ __export(index_exports, {
|
|
|
129
130
|
lgtinClass: () => lgtinClass,
|
|
130
131
|
locationStatusOf: () => locationStatusOf,
|
|
131
132
|
mapRecord: () => mapRecord,
|
|
133
|
+
mapRecordChecked: () => mapRecordChecked,
|
|
132
134
|
meetsTests: () => meetsTests,
|
|
133
135
|
minuteOfDayAt: () => minuteOfDayAt,
|
|
134
136
|
monteCarloForecast: () => monteCarloForecast,
|
|
@@ -1089,6 +1091,15 @@ var ObservedReducer = class {
|
|
|
1089
1091
|
/** 아직 관측되지 않은 자식의 담김 — 물품을 지어내지 않고 보류했다가 등장할 때 붙인다. */
|
|
1090
1092
|
pendingParent = /* @__PURE__ */ new Map();
|
|
1091
1093
|
// 자식 EPC → 부모(물류단위)
|
|
1094
|
+
/**
|
|
1095
|
+
* 팔레트가 **담고 있는 것**(클래스 + 수량) — 바코드 없이 수량만 실린 담김.
|
|
1096
|
+
*
|
|
1097
|
+
* · `qtyAggregation` — 우리가 그 팔레트 물품에 채워 넣은 내용(풀릴 때 되돌릴 근거).
|
|
1098
|
+
* · `pendingQuantities` — 담김이 먼저 오고 그 팔레트를 아직 관측하지 못한 경우. 물품을 지어내지
|
|
1099
|
+
* 않고 보류해 두었다가, 관측되는 순간 붙인다(`pendingParent` 와 같은 규율).
|
|
1100
|
+
*/
|
|
1101
|
+
qtyAggregation = /* @__PURE__ */ new Map();
|
|
1102
|
+
pendingQuantities = /* @__PURE__ */ new Map();
|
|
1092
1103
|
tasks = /* @__PURE__ */ new Map();
|
|
1093
1104
|
equipment = /* @__PURE__ */ new Map();
|
|
1094
1105
|
persons = /* @__PURE__ */ new Map();
|
|
@@ -1379,16 +1390,44 @@ var ObservedReducer = class {
|
|
|
1379
1390
|
this.noteUnhandled(e);
|
|
1380
1391
|
}
|
|
1381
1392
|
}
|
|
1382
|
-
/**
|
|
1383
|
-
|
|
1393
|
+
/**
|
|
1394
|
+
* 반영하지 못한 사건을 종류별로 센다 — 처음·마지막 시각을 함께 남겨 「언제부터」에 답한다.
|
|
1395
|
+
*
|
|
1396
|
+
* `key` 를 주면 그 이름으로 센다: 종류 전체를 못 다룬 것과 **그 종류의 어떤 모양만** 못 다룬 것은
|
|
1397
|
+
* 다른 사실이다(예: 담김은 반영하는데 품번이 섞인 팔레트만 못 담는다).
|
|
1398
|
+
*/
|
|
1399
|
+
noteUnhandled(e, key) {
|
|
1384
1400
|
const at = Date.parse(String(e.eventTime ?? ""));
|
|
1385
|
-
const
|
|
1401
|
+
const name = key ?? e.eventType;
|
|
1402
|
+
const cur = this.unhandled.get(name) ?? { count: 0 };
|
|
1386
1403
|
cur.count++;
|
|
1387
1404
|
if (Number.isFinite(at)) {
|
|
1388
1405
|
if (cur.firstAtMs === void 0 || at < cur.firstAtMs) cur.firstAtMs = at;
|
|
1389
1406
|
if (cur.lastAtMs === void 0 || at > cur.lastAtMs) cur.lastAtMs = at;
|
|
1390
1407
|
}
|
|
1391
|
-
this.unhandled.set(
|
|
1408
|
+
this.unhandled.set(name, cur);
|
|
1409
|
+
}
|
|
1410
|
+
/**
|
|
1411
|
+
* 팔레트 물품에 **담고 있는 것**을 채운다 — 이미 아는 값을 덮지 않는다.
|
|
1412
|
+
*
|
|
1413
|
+
* 팔레트가 자기 품번·수량을 이미 들고 있으면(관측이 그렇게 말했으면) 담김 선언이 그것을 밀어내지
|
|
1414
|
+
* 않는다. 비어 있던 자리만 채운다 — 「모른다」를 채우는 것이 이 작업의 목적이고, 아는 것을 바꾸는
|
|
1415
|
+
* 것은 아니다.
|
|
1416
|
+
*/
|
|
1417
|
+
withContainedQuantity(cur, q, all) {
|
|
1418
|
+
const parsed = q.epcClass ? parseEpc(q.epcClass) : void 0;
|
|
1419
|
+
const declared = all.filter((x) => typeof x.quantity === "number");
|
|
1420
|
+
return {
|
|
1421
|
+
...cur,
|
|
1422
|
+
gtin: cur.gtin ?? q.epcClass,
|
|
1423
|
+
gtinKey: cur.gtinKey ?? parsed?.gtinKey,
|
|
1424
|
+
/* 관측이 준 수량이 있으면 그대로 둔다. 없으면(팔레트 하나로 세어 `1` 이던 자리) 내용의 수량이 답이다. */
|
|
1425
|
+
qty: cur.qty !== void 0 && cur.qty !== 1 ? cur.qty : q.quantity,
|
|
1426
|
+
uom: cur.uom ?? q.uom,
|
|
1427
|
+
/* 단위가 여럿일 때만 목록을 든다(하나면 `qty`/`uom` 이 무손실로 들고 있다 — 위 `upsert` 와 같은 규율). */
|
|
1428
|
+
quantities: cur.quantities ?? (declared.length >= 2 ? declared.map((x) => ({ value: x.quantity, ...x.uom ? { uom: x.uom } : {} })) : void 0),
|
|
1429
|
+
lot: cur.lot ?? parsed?.lot
|
|
1430
|
+
};
|
|
1392
1431
|
}
|
|
1393
1432
|
applyEpcis(ev, envelope) {
|
|
1394
1433
|
if (ev.errorDeclaration) {
|
|
@@ -1401,6 +1440,20 @@ var ObservedReducer = class {
|
|
|
1401
1440
|
return;
|
|
1402
1441
|
}
|
|
1403
1442
|
if (ev.type === "AggregationEvent") {
|
|
1443
|
+
if (ev.action === "ADD" && ev.childQuantityList?.length) {
|
|
1444
|
+
const classes = new Set(ev.childQuantityList.map((q2) => q2.epcClass));
|
|
1445
|
+
if (classes.size > 1) {
|
|
1446
|
+
if (envelope) this.noteUnhandled(envelope, "aggregation-mixed-classes");
|
|
1447
|
+
} else {
|
|
1448
|
+
const q2 = ev.childQuantityList[0];
|
|
1449
|
+
const cur = this.items.get(ev.parentID);
|
|
1450
|
+
if (!cur) this.pendingQuantities.set(ev.parentID, [...ev.childQuantityList]);
|
|
1451
|
+
else {
|
|
1452
|
+
this.items.set(cur.subLotId ?? cur.epc, this.withContainedQuantity(cur, q2, ev.childQuantityList));
|
|
1453
|
+
this.qtyAggregation.set(ev.parentID, [...ev.childQuantityList]);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1404
1457
|
if (ev.action === "ADD" && ev.childEPCs?.length) {
|
|
1405
1458
|
this.aggregation.set(ev.parentID, [...ev.childEPCs]);
|
|
1406
1459
|
for (const child of ev.childEPCs) {
|
|
@@ -1418,6 +1471,14 @@ var ObservedReducer = class {
|
|
|
1418
1471
|
this.pendingParent.delete(child);
|
|
1419
1472
|
}
|
|
1420
1473
|
this.aggregation.delete(ev.parentID);
|
|
1474
|
+
if (this.qtyAggregation.delete(ev.parentID)) {
|
|
1475
|
+
const cur = this.items.get(ev.parentID);
|
|
1476
|
+
if (cur) {
|
|
1477
|
+
const { gtin, gtinKey, qty, uom, quantities, lot, ...rest } = cur;
|
|
1478
|
+
this.items.set(cur.subLotId ?? cur.epc, { ...rest, qty: 1 });
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
this.pendingQuantities.delete(ev.parentID);
|
|
1421
1482
|
}
|
|
1422
1483
|
return;
|
|
1423
1484
|
}
|
|
@@ -1475,7 +1536,8 @@ var ObservedReducer = class {
|
|
|
1475
1536
|
const cur = this.items.get(subLotId ?? epc);
|
|
1476
1537
|
const parsedClass = q?.epcClass ? parseEpc(q.epcClass) : void 0;
|
|
1477
1538
|
const parsedSelf = parseEpc(epc);
|
|
1478
|
-
const
|
|
1539
|
+
const pendingQty = this.pendingQuantities.get(epc);
|
|
1540
|
+
const classUri = q?.epcClass ?? pendingQty?.[0]?.epcClass ?? (parsedSelf.instance ? void 0 : epc);
|
|
1479
1541
|
return {
|
|
1480
1542
|
epc,
|
|
1481
1543
|
...subLotId ? { subLotId } : {},
|
|
@@ -1484,8 +1546,12 @@ var ObservedReducer = class {
|
|
|
1484
1546
|
location: patch.location ?? cur?.location ?? "",
|
|
1485
1547
|
disposition: patch.disposition ?? cur?.disposition,
|
|
1486
1548
|
parent: cur?.parent ?? this.pendingParent.get(epc),
|
|
1487
|
-
|
|
1488
|
-
|
|
1549
|
+
/*
|
|
1550
|
+
* 관측이 수량을 말하지 않았는데 **담김이 먼저 와 있었다면** 그 내용이 답이다(바코드 없이 수량만
|
|
1551
|
+
* 실린 팔레트). 보류해 둔 것을 여기서 붙인다 — 위 `parent` 와 같은 규율이다.
|
|
1552
|
+
*/
|
|
1553
|
+
qty: q?.quantity ?? this.pendingQuantities.get(epc)?.[0]?.quantity ?? cur?.qty,
|
|
1554
|
+
uom: q?.uom ?? this.pendingQuantities.get(epc)?.[0]?.uom ?? cur?.uom,
|
|
1489
1555
|
/*
|
|
1490
1556
|
* 선언된 수량 전부 — 값이 없는 항목은 담지 않는다(모름을 0 으로 만들지 않는다).
|
|
1491
1557
|
*
|
|
@@ -1613,6 +1679,9 @@ var ObservedReducer = class {
|
|
|
1613
1679
|
items: [...this.items.values()].map((i) => ({ ...i })),
|
|
1614
1680
|
aggregation: [...this.aggregation.entries()].map(([parent, children]) => ({ parent, children: [...children] })),
|
|
1615
1681
|
pendingParent: [...this.pendingParent.entries()].map(([child, parent]) => ({ child, parent })),
|
|
1682
|
+
/* 수량으로 담긴 것도 이어받는다 — 되풀어 주지 않는 축이다(원천은 담김을 다시 말해 주지 않는다). */
|
|
1683
|
+
qtyAggregation: [...this.qtyAggregation.entries()].map(([parent, quantities]) => ({ parent, quantities })),
|
|
1684
|
+
pendingQuantities: [...this.pendingQuantities.entries()].map(([parent, quantities]) => ({ parent, quantities })),
|
|
1616
1685
|
tasks: [...this.tasks.values()].map((t) => ({ ...t })),
|
|
1617
1686
|
equipment: [...this.equipment.values()].map((m) => ({ ...m })),
|
|
1618
1687
|
persons: [...this.persons.values()].map((x) => ({ ...x })),
|
|
@@ -1634,6 +1703,8 @@ var ObservedReducer = class {
|
|
|
1634
1703
|
this.items = new Map((cp?.items ?? []).map((i) => [i.epc, { ...i }]));
|
|
1635
1704
|
this.aggregation = new Map((cp?.aggregation ?? []).map((a) => [a.parent, [...a.children]]));
|
|
1636
1705
|
this.pendingParent = new Map((cp?.pendingParent ?? []).map((x) => [x.child, x.parent]));
|
|
1706
|
+
this.qtyAggregation = new Map((cp?.qtyAggregation ?? []).map((x) => [x.parent, x.quantities]));
|
|
1707
|
+
this.pendingQuantities = new Map((cp?.pendingQuantities ?? []).map((x) => [x.parent, x.quantities]));
|
|
1637
1708
|
this.tasks = new Map((cp?.tasks ?? []).map((t) => [t.id, { ...t }]));
|
|
1638
1709
|
this.equipment = new Map((cp?.equipment ?? []).map((m) => [m.id, { ...m }]));
|
|
1639
1710
|
this.persons = new Map((cp?.persons ?? []).map((x) => [x.id, { ...x }]));
|
|
@@ -2684,28 +2755,80 @@ function foldJobResponses(rows) {
|
|
|
2684
2755
|
}
|
|
2685
2756
|
|
|
2686
2757
|
// src/face2-adapter.ts
|
|
2758
|
+
function isAggregationRecord(record) {
|
|
2759
|
+
if (!record || typeof record !== "object") return false;
|
|
2760
|
+
const r = record;
|
|
2761
|
+
return typeof r.parentID === "string" && r.parentID.trim().length > 0 && r.epc === void 0;
|
|
2762
|
+
}
|
|
2687
2763
|
function get(obj, path) {
|
|
2688
2764
|
return path.split(".").reduce((o, k) => o == null ? o : o[k], obj);
|
|
2689
2765
|
}
|
|
2690
|
-
function resolve(v, record) {
|
|
2766
|
+
function resolve(v, record, field, errors) {
|
|
2691
2767
|
if (v === void 0) return void 0;
|
|
2692
|
-
if (v.startsWith("$."))
|
|
2693
|
-
|
|
2694
|
-
|
|
2768
|
+
if (!v.startsWith("$.")) return v;
|
|
2769
|
+
const r = get(record, v.slice(2));
|
|
2770
|
+
if (r === void 0 || r === null) return void 0;
|
|
2771
|
+
if (typeof r === "object") {
|
|
2772
|
+
errors.push(`${field} \uAC00 \uAC12\uC774 \uC544\uB2C8\uB77C ${Array.isArray(r) ? "\uBC30\uC5F4" : "\uAC1D\uCCB4"}\uB85C \uC654\uB2E4(${v}) \u2014 \uCEE4\uB125\uD130\uAC00 \uD45C\uC900 \uAD6C\uC870\uB97C \uADF8\uB300\uB85C \uC2E4\uC5C8\uB2E4: ${JSON.stringify(r).slice(0, 80)}`);
|
|
2773
|
+
return void 0;
|
|
2695
2774
|
}
|
|
2696
|
-
return
|
|
2775
|
+
return String(r);
|
|
2776
|
+
}
|
|
2777
|
+
function resolveList(v, record, field, errors) {
|
|
2778
|
+
if (v === void 0) return [];
|
|
2779
|
+
const r = get(record, v.startsWith("$.") ? v.slice(2) : v);
|
|
2780
|
+
if (r === void 0 || r === null) return [];
|
|
2781
|
+
if (!Array.isArray(r) || r.some((x) => typeof x !== "string")) {
|
|
2782
|
+
errors.push(`${field} \uAC00 \uBB38\uC790\uC5F4 \uBC30\uC5F4\uC774 \uC544\uB2C8\uB2E4(${v}): ${JSON.stringify(r).slice(0, 80)}`);
|
|
2783
|
+
return [];
|
|
2784
|
+
}
|
|
2785
|
+
return r.slice();
|
|
2786
|
+
}
|
|
2787
|
+
function resolveQuantityList(v, record, field, errors) {
|
|
2788
|
+
if (v === void 0) return [];
|
|
2789
|
+
const r = get(record, v.startsWith("$.") ? v.slice(2) : v);
|
|
2790
|
+
if (r === void 0 || r === null) return [];
|
|
2791
|
+
if (!Array.isArray(r) || r.some((x) => !x || typeof x !== "object" || Array.isArray(x))) {
|
|
2792
|
+
errors.push(`${field} \uAC00 \uAC1D\uCCB4 \uBC30\uC5F4\uC774 \uC544\uB2C8\uB2E4(${v}): ${JSON.stringify(r).slice(0, 80)}`);
|
|
2793
|
+
return [];
|
|
2794
|
+
}
|
|
2795
|
+
return r.map((x) => ({ ...x }));
|
|
2796
|
+
}
|
|
2797
|
+
function mapRecordChecked(record, mapping, eventTime) {
|
|
2798
|
+
const errors = [];
|
|
2799
|
+
const action = resolve(mapping.action, record, "action", errors) ?? "";
|
|
2800
|
+
const bizStep = resolve(mapping.bizStep, record, "bizStep", errors) ?? "";
|
|
2801
|
+
const disposition = resolve(mapping.disposition, record, "disposition", errors);
|
|
2802
|
+
const readPoint = resolve(mapping.readPoint, record, "readPoint", errors);
|
|
2803
|
+
const bizLocation = resolve(mapping.bizLocation, record, "bizLocation", errors);
|
|
2804
|
+
if (mapping.type === "AggregationEvent") {
|
|
2805
|
+
const parentID = resolve(mapping.parentID, record, "parentID", errors) ?? "";
|
|
2806
|
+
const childEPCs = resolveList(mapping.childEPCs, record, "childEPCs", errors);
|
|
2807
|
+
const childQuantityList = resolveQuantityList(mapping.childQuantityList, record, "childQuantityList", errors);
|
|
2808
|
+
return {
|
|
2809
|
+
event: aggregationEvent({
|
|
2810
|
+
eventTime,
|
|
2811
|
+
action,
|
|
2812
|
+
bizStep,
|
|
2813
|
+
disposition,
|
|
2814
|
+
parentID,
|
|
2815
|
+
/* 없는 쪽은 **필드를 만들지 않는다** — 빈 배열을 실으면 「자식이 없다」고 말하는 것이 된다. */
|
|
2816
|
+
...childEPCs.length ? { childEPCs } : {},
|
|
2817
|
+
...childQuantityList.length ? { childQuantityList } : {},
|
|
2818
|
+
readPoint,
|
|
2819
|
+
bizLocation
|
|
2820
|
+
}),
|
|
2821
|
+
errors
|
|
2822
|
+
};
|
|
2823
|
+
}
|
|
2824
|
+
const epc = resolve(mapping.epc, record, "epc", errors);
|
|
2825
|
+
return {
|
|
2826
|
+
event: objectEvent({ eventTime, action, bizStep, disposition, epcList: epc ? [epc] : [], readPoint, bizLocation }),
|
|
2827
|
+
errors
|
|
2828
|
+
};
|
|
2697
2829
|
}
|
|
2698
2830
|
function mapRecord(record, mapping, eventTime) {
|
|
2699
|
-
|
|
2700
|
-
return objectEvent({
|
|
2701
|
-
eventTime,
|
|
2702
|
-
action: resolve(mapping.action, record) ?? "",
|
|
2703
|
-
bizStep: resolve(mapping.bizStep, record) ?? "",
|
|
2704
|
-
disposition: resolve(mapping.disposition, record),
|
|
2705
|
-
epcList: epc ? [epc] : [],
|
|
2706
|
-
readPoint: resolve(mapping.readPoint, record),
|
|
2707
|
-
bizLocation: resolve(mapping.bizLocation, record)
|
|
2708
|
-
});
|
|
2831
|
+
return mapRecordChecked(record, mapping, eventTime).event;
|
|
2709
2832
|
}
|
|
2710
2833
|
function ingest(records, rules, opts) {
|
|
2711
2834
|
const ruleByType = new Map(rules.map((r) => [r.sourceType, r]));
|
|
@@ -2718,9 +2841,10 @@ function ingest(records, rules, opts) {
|
|
|
2718
2841
|
rejected.push({ record, errors: [`\uB9E4\uCE6D \uB8F0 \uC5C6\uC74C: sourceType=${record["sourceType"]}`] });
|
|
2719
2842
|
continue;
|
|
2720
2843
|
}
|
|
2721
|
-
const
|
|
2722
|
-
const
|
|
2723
|
-
const errors =
|
|
2844
|
+
const timeErrors = [];
|
|
2845
|
+
const eventTime = (opts.eventTimePath ? resolve("$." + opts.eventTimePath, record, "eventTime", timeErrors) : void 0) ?? opts.defaultEventTime;
|
|
2846
|
+
const { event: ev, errors: mapErrors } = mapRecordChecked(record, rule.mapping, eventTime);
|
|
2847
|
+
const errors = [...timeErrors, ...mapErrors, ...validateEpcisEvent(ev)];
|
|
2724
2848
|
if (errors.length) {
|
|
2725
2849
|
rejected.push({ record, errors });
|
|
2726
2850
|
continue;
|
|
@@ -3861,6 +3985,11 @@ var FlowEngine = class {
|
|
|
3861
3985
|
/* 스냅샷이 **델타보다 가난하면 안 된다** — 예전에는 소요·남은 시간을 빼고 내보내서, 이 스냅샷으로
|
|
3862
3986
|
* 다른 커널을 주입하면(hydrateObserved) 진행 중이던 작업을 이어 굴릴 수 없었다(미러 스냅샷은
|
|
3863
3987
|
* 델타에서 왔으므로 갖고 있었다 — 같은 계약을 두 구동이 다르게 채우던 자리). */
|
|
3988
|
+
/* 관측이 담지 못한 사실을 그대로 내보낸다 — 리듀서가 세고 있어도 여기서 떨어뜨리면 아무도 못 본다. */
|
|
3989
|
+
...(() => {
|
|
3990
|
+
const u = this.observer?.snapshot?.().unhandled;
|
|
3991
|
+
return u?.length ? { unhandled: u } : {};
|
|
3992
|
+
})(),
|
|
3864
3993
|
tasks: [...this.tasks.values()].map((t) => ({
|
|
3865
3994
|
id: t.id,
|
|
3866
3995
|
kind: t.kind,
|
|
@@ -7762,6 +7891,7 @@ function retiredVocabularyIn(line) {
|
|
|
7762
7891
|
ingestEnergyEquipmentRecords,
|
|
7763
7892
|
ingestEnergyRecords,
|
|
7764
7893
|
ingestOperationalRecords,
|
|
7894
|
+
isAggregationRecord,
|
|
7765
7895
|
isElectricalLocationType,
|
|
7766
7896
|
isEnergyEquipmentRecord,
|
|
7767
7897
|
isEnergyRecord,
|
|
@@ -7773,6 +7903,7 @@ function retiredVocabularyIn(line) {
|
|
|
7773
7903
|
lgtinClass,
|
|
7774
7904
|
locationStatusOf,
|
|
7775
7905
|
mapRecord,
|
|
7906
|
+
mapRecordChecked,
|
|
7776
7907
|
meetsTests,
|
|
7777
7908
|
minuteOfDayAt,
|
|
7778
7909
|
monteCarloForecast,
|
package/package.json
CHANGED