@operato/twin-kernel 0.7.37 → 0.7.39
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 +39 -0
- package/dist/ems-kernel.js +39 -1
- 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 +91 -8
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -1245,6 +1245,20 @@ export interface Attention {
|
|
|
1245
1245
|
/** 계량 지점의 마지막 관측 — 값이 없으면 **모르는 것이다**(0 이 아니다). */
|
|
1246
1246
|
export interface MeterPointState {
|
|
1247
1247
|
id: string;
|
|
1248
|
+
/**
|
|
1249
|
+
* 이 계량 지점이 **모델에 선언된 것인가, 계측이 처음 데려온 것인가** (자리와 같은 어휘).
|
|
1250
|
+
*
|
|
1251
|
+
* ── 왜 표시해야 하나 ───────────────────────────────────────────────────────
|
|
1252
|
+
* 계측은 모르는 `meterId` 를 만나면 **조용히 새 지점을 만든다.** 그 kW 는 그 현장의 수요·피크에
|
|
1253
|
+
* 그대로 더해지므로(그것이 옳다 — 빼면 「계약 안쪽」이라는 더 위험한 거짓이 된다), 표시가 없으면
|
|
1254
|
+
* 커넥터 매핑에서 id 를 한 글자 틀린 것과 정상을 구별할 수 없다. 그때 진짜 계량기는 조용히 멈추고
|
|
1255
|
+
* 유령이 누적되며, **숫자는 그럴듯하고 틀린다.** 요금 판정까지 가는 축이라 자리 오타보다 무겁다.
|
|
1256
|
+
*
|
|
1257
|
+
* `'master'` = 모델(설비 선언)에 있는 계량기. `'observed'` = 계측만 데려온 계량기.
|
|
1258
|
+
* 옛 스냅샷에는 이 칸이 없다(`undefined`) — 「선언되었다」로 읽지 말 것. 선언 여부의 정본은 모델이고,
|
|
1259
|
+
* 커널은 표본이 올 때마다 이 값을 다시 맞춘다.
|
|
1260
|
+
*/
|
|
1261
|
+
origin?: 'master' | 'observed';
|
|
1248
1262
|
kW?: number;
|
|
1249
1263
|
/** 누적 전력량(원천이 준 적산값) — 우리가 적분한 값이 아니다. */
|
|
1250
1264
|
kWh?: number;
|
|
@@ -1265,6 +1279,18 @@ export interface MeterPointState {
|
|
|
1265
1279
|
export interface DemandWindowState {
|
|
1266
1280
|
startMs: number;
|
|
1267
1281
|
endMs: number;
|
|
1282
|
+
/**
|
|
1283
|
+
* `maxKW` 가 찍힌 **그 순간**의, **모델이 선언한** 뿌리 계량기들만의 합.
|
|
1284
|
+
*
|
|
1285
|
+
* `maxKW` 는 모르는 계량기까지 포함한 합이다(그것을 빼면 부하가 조용히 작아지고, 작아진 부하는
|
|
1286
|
+
* 「계약 안쪽」이라는 더 위험한 거짓을 만든다). 그러나 그 값 하나만 내면 반대쪽 거짓이 생긴다 —
|
|
1287
|
+
* 오타로 생긴 유령 계량기가 계약 초과를 만들어도 화면은 그것을 진짜 초과로 말한다.
|
|
1288
|
+
*
|
|
1289
|
+
* 그래서 **두 벌을 낸다.** 화면은 「선언된 것으로는 82%, 정체 모를 계량기까지 넣으면 104%」라고
|
|
1290
|
+
* 말할 수 있다 — 빼서 정확한 척하지 않고, 더해서 뭉개지도 않는다. 두 값이 같으면 모든 뿌리가
|
|
1291
|
+
* 선언된 것이고, 그것도 사실이다. 잰 적이 없으면 `undefined` 다(0 이 아니다).
|
|
1292
|
+
*/
|
|
1293
|
+
maxDeclaredKW?: number;
|
|
1268
1294
|
/**
|
|
1269
1295
|
* 이 구간의 **최대가 찍힌 순간**의 지점별 kW — 요금이 걸린 수는 kW 다.
|
|
1270
1296
|
*
|
|
@@ -1397,6 +1423,19 @@ export interface StateSnapshot {
|
|
|
1397
1423
|
tasks: TaskState[];
|
|
1398
1424
|
orders: OrderState[];
|
|
1399
1425
|
attentions?: Attention[];
|
|
1426
|
+
/**
|
|
1427
|
+
* **담을 줄 몰라 반영하지 못한 사실** — 종류별 수(관측 구동에서만 나온다).
|
|
1428
|
+
*
|
|
1429
|
+
* 관측 리듀서는 오래전부터 이것을 세어 왔다. 그런데 **스냅샷이 그 값을 떨어뜨렸다** — 그래서
|
|
1430
|
+
* 이미 그것을 읽도록 쓰여 있던 정합성 검사(`twin-unhandled-vocabulary`)는 미러에서 영원히
|
|
1431
|
+
* 조용했다(2026-08-19 실측). 세기만 하고 아무도 못 보는 값은 없는 것과 같다.
|
|
1432
|
+
*/
|
|
1433
|
+
unhandled?: {
|
|
1434
|
+
eventType: string;
|
|
1435
|
+
count: number;
|
|
1436
|
+
firstAtMs?: number;
|
|
1437
|
+
lastAtMs?: number;
|
|
1438
|
+
}[];
|
|
1400
1439
|
/**
|
|
1401
1440
|
* 확인(ack)해 둔 주목 신호 id — **상태에서 파생되지 않는 유일한 축.**
|
|
1402
1441
|
*
|
package/dist/ems-kernel.js
CHANGED
|
@@ -286,6 +286,20 @@ export class EmsKernel extends FlowEngine {
|
|
|
286
286
|
this.openWindow(atMs);
|
|
287
287
|
const kW = Number.isFinite(Number(d?.kW)) ? Number(d.kW) : undefined;
|
|
288
288
|
const point = this.points.get(id) ?? { id, samplesInWindow: 0 };
|
|
289
|
+
/*
|
|
290
|
+
* ── 이 계량기가 모델에 있는가를 **표시한다** (2026-08-20) ────────────────────
|
|
291
|
+
*
|
|
292
|
+
* 모르는 `meterId` 는 여기서 조용히 새 지점이 된다. 그 kW 는 수요·피크에 그대로 더해지는데(그것이
|
|
293
|
+
* 옳다 — 아래 뿌리 합 주석), 표시가 없으면 커넥터 매핑의 한 글자 오타와 정상을 구별할 수 없다.
|
|
294
|
+
* 그때 진짜 계량기는 조용히 멈추고 유령이 누적되며, 숫자는 그럴듯하고 틀린다.
|
|
295
|
+
*
|
|
296
|
+
* 바로 옆 `applyEquipmentEnergy` 는 같은 상황을 이미 세어 두고 있었다(`unknownEquipmentReports`).
|
|
297
|
+
* 계량기만 그 규율에서 빠져 있었다 — 요금까지 가는 축이라 오히려 더 무거운 자리다.
|
|
298
|
+
*
|
|
299
|
+
* **표본마다 다시 맞춘다**: 선언 여부의 정본은 모델이고, 모델은 채택(adopt)으로 늘어난다. 한 번
|
|
300
|
+
* 찍고 두면 나중에 선언된 계량기가 영원히 `observed` 로 남는다. 자리(`origin: 'observed'`)와 같은 어휘다.
|
|
301
|
+
*/
|
|
302
|
+
point.origin = this.equipment.has(id) ? 'master' : 'observed';
|
|
289
303
|
/* 늦게 온 옛 표본이 최신 관측을 덮지 않게 — 상위 커널의 `stale` 판정과 같은 규율. */
|
|
290
304
|
if (point.atMs === undefined || atMs >= point.atMs) {
|
|
291
305
|
point.atMs = atMs;
|
|
@@ -324,9 +338,33 @@ export class EmsKernel extends FlowEngine {
|
|
|
324
338
|
* 조용히 작아지고, 작아진 부하는 「계약 안쪽」이라는 더 위험한 거짓을 만든다.
|
|
325
339
|
*/
|
|
326
340
|
const roots = this.rootMeterIds();
|
|
327
|
-
|
|
341
|
+
/*
|
|
342
|
+
* ── 합을 **두 벌** 낸다 (2026-08-20) ──────────────────────────────────────
|
|
343
|
+
*
|
|
344
|
+
* 전부의 합(`total`)이 요금 판정의 근거다 — 모르는 계량기를 빼면 부하가 조용히 작아지고, 작아진
|
|
345
|
+
* 부하는 「계약 안쪽」이라는 더 위험한 거짓을 만든다.
|
|
346
|
+
*
|
|
347
|
+
* 그런데 그 값 하나만 내면 반대쪽 거짓이 생긴다: 오타로 생긴 유령 계량기가 만든 초과를 화면이
|
|
348
|
+
* 진짜 초과로 말한다. 그래서 **선언된 뿌리만의 합**도 같은 순간에 잰다. 화면은 두 수를 나란히
|
|
349
|
+
* 놓고 「선언된 것으로는 82%, 정체 모를 계량기까지 넣으면 104%」라고 말할 수 있다.
|
|
350
|
+
*
|
|
351
|
+
* 선언 여부는 **모델에 직접 묻는다**(`this.equipment`). 지점에 찍힌 `origin` 을 쓰지 않는 이유는
|
|
352
|
+
* 옛 스냅샷에서 이어받은 지점에는 그 칸이 없고, 없는 것을 「선언되었다」로 읽으면 이 값이 조용히
|
|
353
|
+
* 전부의 합과 같아지기 때문이다.
|
|
354
|
+
*/
|
|
355
|
+
let total = 0;
|
|
356
|
+
let declared = 0;
|
|
357
|
+
for (const p of this.points.values()) {
|
|
358
|
+
if (!roots.has(p.id))
|
|
359
|
+
continue;
|
|
360
|
+
const v = p.kW ?? 0;
|
|
361
|
+
total += v;
|
|
362
|
+
if (this.equipment.has(p.id))
|
|
363
|
+
declared += v;
|
|
364
|
+
}
|
|
328
365
|
if (w.maxKW === undefined || total > w.maxKW) {
|
|
329
366
|
w.maxKW = total;
|
|
367
|
+
w.maxDeclaredKW = declared;
|
|
330
368
|
/*
|
|
331
369
|
* ── 피크가 갱신된 **그 순간**의 지점별 kW 를 붙잡는다 (2026-08-18) ─────────
|
|
332
370
|
*
|
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
|
@@ -1091,6 +1091,15 @@ var ObservedReducer = class {
|
|
|
1091
1091
|
/** 아직 관측되지 않은 자식의 담김 — 물품을 지어내지 않고 보류했다가 등장할 때 붙인다. */
|
|
1092
1092
|
pendingParent = /* @__PURE__ */ new Map();
|
|
1093
1093
|
// 자식 EPC → 부모(물류단위)
|
|
1094
|
+
/**
|
|
1095
|
+
* 팔레트가 **담고 있는 것**(클래스 + 수량) — 바코드 없이 수량만 실린 담김.
|
|
1096
|
+
*
|
|
1097
|
+
* · `qtyAggregation` — 우리가 그 팔레트 물품에 채워 넣은 내용(풀릴 때 되돌릴 근거).
|
|
1098
|
+
* · `pendingQuantities` — 담김이 먼저 오고 그 팔레트를 아직 관측하지 못한 경우. 물품을 지어내지
|
|
1099
|
+
* 않고 보류해 두었다가, 관측되는 순간 붙인다(`pendingParent` 와 같은 규율).
|
|
1100
|
+
*/
|
|
1101
|
+
qtyAggregation = /* @__PURE__ */ new Map();
|
|
1102
|
+
pendingQuantities = /* @__PURE__ */ new Map();
|
|
1094
1103
|
tasks = /* @__PURE__ */ new Map();
|
|
1095
1104
|
equipment = /* @__PURE__ */ new Map();
|
|
1096
1105
|
persons = /* @__PURE__ */ new Map();
|
|
@@ -1381,16 +1390,44 @@ var ObservedReducer = class {
|
|
|
1381
1390
|
this.noteUnhandled(e);
|
|
1382
1391
|
}
|
|
1383
1392
|
}
|
|
1384
|
-
/**
|
|
1385
|
-
|
|
1393
|
+
/**
|
|
1394
|
+
* 반영하지 못한 사건을 종류별로 센다 — 처음·마지막 시각을 함께 남겨 「언제부터」에 답한다.
|
|
1395
|
+
*
|
|
1396
|
+
* `key` 를 주면 그 이름으로 센다: 종류 전체를 못 다룬 것과 **그 종류의 어떤 모양만** 못 다룬 것은
|
|
1397
|
+
* 다른 사실이다(예: 담김은 반영하는데 품번이 섞인 팔레트만 못 담는다).
|
|
1398
|
+
*/
|
|
1399
|
+
noteUnhandled(e, key) {
|
|
1386
1400
|
const at = Date.parse(String(e.eventTime ?? ""));
|
|
1387
|
-
const
|
|
1401
|
+
const name = key ?? e.eventType;
|
|
1402
|
+
const cur = this.unhandled.get(name) ?? { count: 0 };
|
|
1388
1403
|
cur.count++;
|
|
1389
1404
|
if (Number.isFinite(at)) {
|
|
1390
1405
|
if (cur.firstAtMs === void 0 || at < cur.firstAtMs) cur.firstAtMs = at;
|
|
1391
1406
|
if (cur.lastAtMs === void 0 || at > cur.lastAtMs) cur.lastAtMs = at;
|
|
1392
1407
|
}
|
|
1393
|
-
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
|
+
};
|
|
1394
1431
|
}
|
|
1395
1432
|
applyEpcis(ev, envelope) {
|
|
1396
1433
|
if (ev.errorDeclaration) {
|
|
@@ -1403,6 +1440,20 @@ var ObservedReducer = class {
|
|
|
1403
1440
|
return;
|
|
1404
1441
|
}
|
|
1405
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
|
+
}
|
|
1406
1457
|
if (ev.action === "ADD" && ev.childEPCs?.length) {
|
|
1407
1458
|
this.aggregation.set(ev.parentID, [...ev.childEPCs]);
|
|
1408
1459
|
for (const child of ev.childEPCs) {
|
|
@@ -1420,6 +1471,14 @@ var ObservedReducer = class {
|
|
|
1420
1471
|
this.pendingParent.delete(child);
|
|
1421
1472
|
}
|
|
1422
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);
|
|
1423
1482
|
}
|
|
1424
1483
|
return;
|
|
1425
1484
|
}
|
|
@@ -1477,7 +1536,8 @@ var ObservedReducer = class {
|
|
|
1477
1536
|
const cur = this.items.get(subLotId ?? epc);
|
|
1478
1537
|
const parsedClass = q?.epcClass ? parseEpc(q.epcClass) : void 0;
|
|
1479
1538
|
const parsedSelf = parseEpc(epc);
|
|
1480
|
-
const
|
|
1539
|
+
const pendingQty = this.pendingQuantities.get(epc);
|
|
1540
|
+
const classUri = q?.epcClass ?? pendingQty?.[0]?.epcClass ?? (parsedSelf.instance ? void 0 : epc);
|
|
1481
1541
|
return {
|
|
1482
1542
|
epc,
|
|
1483
1543
|
...subLotId ? { subLotId } : {},
|
|
@@ -1486,8 +1546,12 @@ var ObservedReducer = class {
|
|
|
1486
1546
|
location: patch.location ?? cur?.location ?? "",
|
|
1487
1547
|
disposition: patch.disposition ?? cur?.disposition,
|
|
1488
1548
|
parent: cur?.parent ?? this.pendingParent.get(epc),
|
|
1489
|
-
|
|
1490
|
-
|
|
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,
|
|
1491
1555
|
/*
|
|
1492
1556
|
* 선언된 수량 전부 — 값이 없는 항목은 담지 않는다(모름을 0 으로 만들지 않는다).
|
|
1493
1557
|
*
|
|
@@ -1615,6 +1679,9 @@ var ObservedReducer = class {
|
|
|
1615
1679
|
items: [...this.items.values()].map((i) => ({ ...i })),
|
|
1616
1680
|
aggregation: [...this.aggregation.entries()].map(([parent, children]) => ({ parent, children: [...children] })),
|
|
1617
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 })),
|
|
1618
1685
|
tasks: [...this.tasks.values()].map((t) => ({ ...t })),
|
|
1619
1686
|
equipment: [...this.equipment.values()].map((m) => ({ ...m })),
|
|
1620
1687
|
persons: [...this.persons.values()].map((x) => ({ ...x })),
|
|
@@ -1636,6 +1703,8 @@ var ObservedReducer = class {
|
|
|
1636
1703
|
this.items = new Map((cp?.items ?? []).map((i) => [i.epc, { ...i }]));
|
|
1637
1704
|
this.aggregation = new Map((cp?.aggregation ?? []).map((a) => [a.parent, [...a.children]]));
|
|
1638
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]));
|
|
1639
1708
|
this.tasks = new Map((cp?.tasks ?? []).map((t) => [t.id, { ...t }]));
|
|
1640
1709
|
this.equipment = new Map((cp?.equipment ?? []).map((m) => [m.id, { ...m }]));
|
|
1641
1710
|
this.persons = new Map((cp?.persons ?? []).map((x) => [x.id, { ...x }]));
|
|
@@ -3916,6 +3985,11 @@ var FlowEngine = class {
|
|
|
3916
3985
|
/* 스냅샷이 **델타보다 가난하면 안 된다** — 예전에는 소요·남은 시간을 빼고 내보내서, 이 스냅샷으로
|
|
3917
3986
|
* 다른 커널을 주입하면(hydrateObserved) 진행 중이던 작업을 이어 굴릴 수 없었다(미러 스냅샷은
|
|
3918
3987
|
* 델타에서 왔으므로 갖고 있었다 — 같은 계약을 두 구동이 다르게 채우던 자리). */
|
|
3988
|
+
/* 관측이 담지 못한 사실을 그대로 내보낸다 — 리듀서가 세고 있어도 여기서 떨어뜨리면 아무도 못 본다. */
|
|
3989
|
+
...(() => {
|
|
3990
|
+
const u = this.observer?.snapshot?.().unhandled;
|
|
3991
|
+
return u?.length ? { unhandled: u } : {};
|
|
3992
|
+
})(),
|
|
3919
3993
|
tasks: [...this.tasks.values()].map((t) => ({
|
|
3920
3994
|
id: t.id,
|
|
3921
3995
|
kind: t.kind,
|
|
@@ -6606,6 +6680,7 @@ var EmsKernel = class extends FlowEngine {
|
|
|
6606
6680
|
this.openWindow(atMs);
|
|
6607
6681
|
const kW = Number.isFinite(Number(d?.kW)) ? Number(d.kW) : void 0;
|
|
6608
6682
|
const point = this.points.get(id) ?? { id, samplesInWindow: 0 };
|
|
6683
|
+
point.origin = this.equipment.has(id) ? "master" : "observed";
|
|
6609
6684
|
if (point.atMs === void 0 || atMs >= point.atMs) {
|
|
6610
6685
|
point.atMs = atMs;
|
|
6611
6686
|
if (kW !== void 0) point.kW = kW;
|
|
@@ -6618,9 +6693,17 @@ var EmsKernel = class extends FlowEngine {
|
|
|
6618
6693
|
const w = this.open;
|
|
6619
6694
|
if (kW !== void 0) {
|
|
6620
6695
|
const roots = this.rootMeterIds();
|
|
6621
|
-
|
|
6696
|
+
let total = 0;
|
|
6697
|
+
let declared = 0;
|
|
6698
|
+
for (const p of this.points.values()) {
|
|
6699
|
+
if (!roots.has(p.id)) continue;
|
|
6700
|
+
const v = p.kW ?? 0;
|
|
6701
|
+
total += v;
|
|
6702
|
+
if (this.equipment.has(p.id)) declared += v;
|
|
6703
|
+
}
|
|
6622
6704
|
if (w.maxKW === void 0 || total > w.maxKW) {
|
|
6623
6705
|
w.maxKW = total;
|
|
6706
|
+
w.maxDeclaredKW = declared;
|
|
6624
6707
|
w.pointsAtPeak = [...this.points.values()].filter((p) => Number.isFinite(Number(p.kW))).map((p) => ({ id: p.id, kW: Number(p.kW) }));
|
|
6625
6708
|
}
|
|
6626
6709
|
w.samples++;
|
package/package.json
CHANGED