@operato/twin-kernel 0.7.67 → 0.7.69
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/capability.d.ts +4 -0
- package/dist/capability.js +1 -1
- package/dist/contract.d.ts +48 -0
- package/dist/ems-kernel.js +23 -2
- package/dist/energy-ingest.d.ts +11 -0
- package/dist/energy-ingest.js +19 -1
- package/dist/epcis.d.ts +29 -0
- package/dist/epcis.js +31 -0
- package/dist/flow-engine.d.ts +11 -0
- package/dist/flow-engine.js +21 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/master-data.d.ts +80 -0
- package/dist/master-data.js +168 -0
- package/dist/observed-reducer.d.ts +33 -0
- package/dist/observed-reducer.js +104 -4
- package/dist-cjs/index.cjs +235 -8
- package/package.json +1 -1
package/dist/capability.d.ts
CHANGED
|
@@ -53,6 +53,10 @@ export interface GeneratingState {
|
|
|
53
53
|
exportKW?: number;
|
|
54
54
|
generatedKWh?: number;
|
|
55
55
|
generatedKWhResetAt?: string;
|
|
56
|
+
/** 지금 든 적산이 무엇부터 쌓인 것인가 — 원본이 말했을 때만 있다. */
|
|
57
|
+
generatedKWhSince?: string;
|
|
58
|
+
/** 되돌아간 판정이 사실인가 짐작인가 — 'declared' 또는 'inferred'. */
|
|
59
|
+
generatedKWhBasis?: 'declared' | 'inferred';
|
|
56
60
|
}
|
|
57
61
|
/** 저장 — 담고 낸다. 충전과 방전을 나눈다(손실·수명 판단이 둘을 구별해야 한다). */
|
|
58
62
|
export interface StoringState {
|
package/dist/capability.js
CHANGED
|
@@ -86,7 +86,7 @@ export const CAPABILITIES = {
|
|
|
86
86
|
* 계량 능력(`metered`)이 `kW` 와 `kWh` 를 함께 선언한 것과 같은 짝이다. 적산은 발전 설비의 보편적
|
|
87
87
|
* 사실이고(태양광·열병합·디젤), 그것이 없으면 성능비·발전시간 같은 성과를 아무도 셀 수 없다.
|
|
88
88
|
*/
|
|
89
|
-
stateFields: ['generatedKW', 'exportKW', 'generatedKWh', 'generatedKWhResetAt'], models: ['GeneratingState'], results: ['generated']
|
|
89
|
+
stateFields: ['generatedKW', 'exportKW', 'generatedKWh', 'generatedKWhResetAt', 'generatedKWhSince', 'generatedKWhBasis'], models: ['GeneratingState'], results: ['generated']
|
|
90
90
|
},
|
|
91
91
|
/*
|
|
92
92
|
* ── 왜 `storing` 이 아니라 `energyStoring` 인가 (2026-08-15) ────────────────
|
package/dist/contract.d.ts
CHANGED
|
@@ -1378,6 +1378,23 @@ export interface EquipmentState extends EffectivePeriod {
|
|
|
1378
1378
|
* 되돌아간 적이 없으면 이 칸이 없다.
|
|
1379
1379
|
*/
|
|
1380
1380
|
generatedKWhResetAt?: ISOTime;
|
|
1381
|
+
/**
|
|
1382
|
+
* 지금 든 적산이 **무엇부터 쌓인 것인가** — 원본이 말했을 때만 있다 (2026-08-28).
|
|
1383
|
+
*
|
|
1384
|
+
* 이 값이 있으면 차분을 구하는 쪽이 구간을 안다. 그리고 값이 줄었을 때 그것이 계기의 이상인지
|
|
1385
|
+
* (기준점 그대로) 구간이 새로 시작한 것인지(기준점이 나아갔다) 갈릴 수 있다.
|
|
1386
|
+
*/
|
|
1387
|
+
generatedKWhSince?: ISOTime;
|
|
1388
|
+
/**
|
|
1389
|
+
* `generatedKWhResetAt` 이 **사실인가 짐작인가**.
|
|
1390
|
+
*
|
|
1391
|
+
* 'declared' 기준점을 받았고 그것이 그대로인데 값이 줄었다 — 계기의 이상이다
|
|
1392
|
+
* 'inferred' 기준점을 받지 못했고 값이 줄었다 — 계기 교체인지 구간 경계인지 **모른다**
|
|
1393
|
+
*
|
|
1394
|
+
* 예전에는 둘을 구별하지 않고 언제나 「되돌아갔다」로 적었다. 그러면 하루 누적을 주는 원본에서 매일
|
|
1395
|
+
* 자정이 계기 교체로 보인다. 짐작을 사실처럼 적지 않기 위한 칸이다.
|
|
1396
|
+
*/
|
|
1397
|
+
generatedKWhBasis?: 'declared' | 'inferred';
|
|
1381
1398
|
/** storing — 충전율(%)·충전·방전(kW). 충전과 방전을 나눈다(손실·수명 판단이 그 둘을 구별한다). */
|
|
1382
1399
|
soc?: number;
|
|
1383
1400
|
chargeKW?: number;
|
|
@@ -2405,6 +2422,29 @@ export interface EnergyGeneratedData {
|
|
|
2405
2422
|
* 다르며 다를 때 맞는 쪽은 계기다(계량 쪽과 같은 규율).
|
|
2406
2423
|
*/
|
|
2407
2424
|
kWh: number;
|
|
2425
|
+
/**
|
|
2426
|
+
* **이 적산이 쌓이기 시작한 시각** — 원본이 말했을 때만 있다 (2026-08-28).
|
|
2427
|
+
*
|
|
2428
|
+
* ── 왜 필요한가 ─────────────────────────────────────────────────────────────
|
|
2429
|
+
* 계기가 주는 적산이 어느 구간의 것인지는 **원본마다 다르다** — 설치 이후·하루·한 달·요금기간.
|
|
2430
|
+
* 표준도 그것을 갈라 둔다(계측값에 구간을 붙인다). 그런데 이 문은 한 뜻만 가정하고 있었다:
|
|
2431
|
+
* 단조 증가하는 총적산. 그래서 값이 줄면 **계기 교체로 추론했다.**
|
|
2432
|
+
*
|
|
2433
|
+
* 하루 누적을 주는 원본에서는 그 추론이 매일 자정마다 참이 된다 — 실측(PPMS 인버터):
|
|
2434
|
+
*
|
|
2435
|
+
* 2026-08-25 23:37 KST acc 574.8
|
|
2436
|
+
* 2026-08-28 18:25 KST acc 165.2 ← 줄었다. 자정에 되돌아간다
|
|
2437
|
+
*
|
|
2438
|
+
* 그대로 받으면 「계기 교체가 하루에 한 번 일어난다」가 기록된다.
|
|
2439
|
+
*
|
|
2440
|
+
* ── 왜 시각인가(구간의 이름이 아니라) ───────────────────────────────────────
|
|
2441
|
+
* 한 축으로 하루·한 달·요금기간·설치 이후를 다 담고, 커널이 구간을 이어 붙일 근거도 그 시각이다.
|
|
2442
|
+
* 「요금기간」 같은 이름은 경계를 계약이 정하므로 이름만으로는 커널이 알 수 없다.
|
|
2443
|
+
*
|
|
2444
|
+
* **없으면 「모른다」다** — 설치 이후로 가정하지 않는다. 그때 커널은 지금처럼 추론하고, 그 추론이
|
|
2445
|
+
* 짐작이라는 사실을 상태에 남긴다(§`generatedKWhBasis`).
|
|
2446
|
+
*/
|
|
2447
|
+
since?: ISOTime;
|
|
2408
2448
|
at: ISOTime;
|
|
2409
2449
|
}
|
|
2410
2450
|
/** 계량 도착의 실린 값 — 계량 지점 하나의 한 시점. */
|
|
@@ -2415,6 +2455,14 @@ export interface EnergyMeasuredData {
|
|
|
2415
2455
|
kW: number;
|
|
2416
2456
|
/** 누적 전력량(kWh) — 계기 누적값. 차분은 소비처가 한다(계기 교체·리셋을 우리가 지어내지 않는다). */
|
|
2417
2457
|
kWh?: number;
|
|
2458
|
+
/**
|
|
2459
|
+
* 이 적산이 쌓이기 시작한 시각 — 원본이 말했을 때만 있다(§`EnergyGeneratedData.since`).
|
|
2460
|
+
*
|
|
2461
|
+
* 계량 쪽의 셈은 이 값 없이도 정직하다 — 적산이 줄면 그 구간의 전력량을 **내지 않는다**(음수를
|
|
2462
|
+
* 만들지 않는다). 이 축은 셈을 바꾸지 않고 **왜 비는지**를 말할 수 있게 한다: 「계기가 교체돼서」와
|
|
2463
|
+
* 「구간이 바뀌어서」는 다른 사실이고, 화면이 그 둘을 같은 빈칸으로 보이면 사람이 계기를 의심한다.
|
|
2464
|
+
*/
|
|
2465
|
+
kWhSince?: ISOTime;
|
|
2418
2466
|
/** 역률 — 없으면 모르는 것이다(1 로 채우지 않는다). */
|
|
2419
2467
|
powerFactor?: number;
|
|
2420
2468
|
at: ISOTime;
|
package/dist/ems-kernel.js
CHANGED
|
@@ -322,13 +322,34 @@ export class EmsKernel extends FlowEngine {
|
|
|
322
322
|
if (heardAt !== undefined && atMs < heardAt)
|
|
323
323
|
return;
|
|
324
324
|
this.generationHeardAtMs.set(id, atMs);
|
|
325
|
+
/*
|
|
326
|
+
* ── 되돌아간 적산 — **세 갈래로 가른다** (2026-08-28) ───────────────────────
|
|
327
|
+
*
|
|
328
|
+
* 예전에는 갈래가 하나였다: 값이 줄면 계기 교체다. 그런데 **하루 누적을 주는 원본**에서는 그
|
|
329
|
+
* 판정이 매일 자정마다 참이 된다 — 실측(PPMS 인버터): 574.8 → 165.2, 자정에 되돌아간다.
|
|
330
|
+
* 그러면 「계기 교체가 하루에 한 번 일어난다」가 지난 기록에 남는다.
|
|
331
|
+
*
|
|
332
|
+
* 원본이 **기준점**을 말해 주면 그 추론이 필요 없어진다(§`EnergyGeneratedData.since`).
|
|
333
|
+
*
|
|
334
|
+
* 기준점 그대로 · 값이 줄었다 계기의 이상이다 → resetAt · basis 'declared'
|
|
335
|
+
* 기준점이 나아갔다 구간이 새로 시작했다 → resetAt 을 찍지 않는다
|
|
336
|
+
* 기준점이 없다 · 값이 줄었다 모른다 → resetAt · basis 'inferred'
|
|
337
|
+
*
|
|
338
|
+
* 셋째가 예전의 전부였다. 짐작을 사실처럼 적지 않기 위해 그것에 표를 남긴다.
|
|
339
|
+
*/
|
|
325
340
|
const before = Number(eq.generatedKWh);
|
|
326
|
-
|
|
341
|
+
const priorSince = eq.generatedKWhSince;
|
|
342
|
+
const since = typeof d?.since === 'string' && d.since.trim() ? d.since.trim() : undefined;
|
|
343
|
+
const periodAdvanced = !!since && !!priorSince && Date.parse(since) > Date.parse(priorSince);
|
|
344
|
+
if (Number.isFinite(before) && kWh < before && !periodAdvanced) {
|
|
327
345
|
/* 되돌아갔다 — 값은 새것으로 두고, 그 사실을 시각으로 남긴다(우리가 계단을 메우지 않는다). */
|
|
328
346
|
;
|
|
329
347
|
eq.generatedKWhResetAt = new Date(atMs).toISOString();
|
|
348
|
+
eq.generatedKWhBasis = since ? 'declared' : 'inferred';
|
|
330
349
|
}
|
|
331
|
-
|
|
350
|
+
/* 기준점은 온 것만 세운다 — 오지 않았다고 앞서 받은 것을 지우지 않는다(결측 ≠ 없음). */
|
|
351
|
+
if (since)
|
|
352
|
+
eq.generatedKWhSince = since;
|
|
332
353
|
eq.generatedKWh = kWh;
|
|
333
354
|
eq.measuredAt = new Date(atMs).toISOString();
|
|
334
355
|
this.revision++;
|
package/dist/energy-ingest.d.ts
CHANGED
|
@@ -65,6 +65,17 @@ export interface EnergyGenerationRecord {
|
|
|
65
65
|
equipmentId: string;
|
|
66
66
|
/** 계기 적산값(kWh) — 연결된 시스템이 준 값 그대로. 차분은 소비처가 한다. */
|
|
67
67
|
kWh?: number;
|
|
68
|
+
/**
|
|
69
|
+
* **이 적산이 쌓이기 시작한 시각**(ISO) — 원본이 그 뜻을 말할 때만 싣는다 (2026-08-28).
|
|
70
|
+
*
|
|
71
|
+
* 하루 누적을 주는 원본이 있다. 그 값을 총적산으로 받으면 **매일 자정이 계기 교체로 기록된다**
|
|
72
|
+
* (§`EnergyGeneratedData.since` 의 실측).
|
|
73
|
+
*
|
|
74
|
+
* **없으면 비운다** — 설치 이후라고 가정하지 않는다. 원본이 말하지 않은 것을 커넥터가 지어내지
|
|
75
|
+
* 않는 것이 이 문의 규율이다. 다만 원본의 **문서나 코드가 그 뜻을 말한다면** 그것을 커널 어휘로
|
|
76
|
+
* 옮기는 것은 지어내기가 아니다 — 그때 현장의 선언된 시간대로 그 구간의 시작을 적는다.
|
|
77
|
+
*/
|
|
78
|
+
kWhSince?: string;
|
|
68
79
|
at?: string;
|
|
69
80
|
}
|
|
70
81
|
/** 이 레코드가 발전 적산인가 — 어느 갈래로 보낼지를 한 곳에서 정한다. */
|
package/dist/energy-ingest.js
CHANGED
|
@@ -233,11 +233,29 @@ export function ingestEnergyGenerationRecords(records, opts) {
|
|
|
233
233
|
const eventTime = String(r?.at ?? '').trim() || opts.defaultEventTime || '';
|
|
234
234
|
if (!eventTime)
|
|
235
235
|
errors.push('at 이 없고 기본 시각도 주지 않았다 — 언제 잰 것인지 지어낼 수 없다');
|
|
236
|
+
/*
|
|
237
|
+
* 기준점은 **말했을 때만** 받는다. 형식이 틀리면 거부한다 — 못 읽는 기준점을 통과시키면 커널이
|
|
238
|
+
* 그것을 「기준점이 없다」로 읽고 다시 추론으로 떨어지는데, 커넥터는 알렸다고 믿는다.
|
|
239
|
+
*/
|
|
240
|
+
let since;
|
|
241
|
+
const rawSince = r?.kWhSince;
|
|
242
|
+
if (rawSince !== undefined && rawSince !== null && String(rawSince).trim()) {
|
|
243
|
+
const text = String(rawSince).trim();
|
|
244
|
+
const at = Date.parse(text);
|
|
245
|
+
if (!Number.isFinite(at))
|
|
246
|
+
errors.push(`kWhSince 를 시각으로 읽을 수 없다: ${JSON.stringify(rawSince)}`);
|
|
247
|
+
else if (Number.isFinite(Date.parse(eventTime)) && at > Date.parse(eventTime)) {
|
|
248
|
+
/* 미래에서 쌓이기 시작한 적산은 없다 — 두 값 중 하나가 틀렸고, 어느 쪽인지 우리가 고를 수 없다. */
|
|
249
|
+
errors.push(`kWhSince(${text}) 가 잰 시각(${eventTime})보다 뒤다 — 둘 중 하나가 틀렸다`);
|
|
250
|
+
}
|
|
251
|
+
else
|
|
252
|
+
since = text;
|
|
253
|
+
}
|
|
236
254
|
if (errors.length) {
|
|
237
255
|
rejected.push({ record: r, errors });
|
|
238
256
|
continue;
|
|
239
257
|
}
|
|
240
|
-
const data = { equipmentId, kWh, at: eventTime };
|
|
258
|
+
const data = { equipmentId, kWh, at: eventTime, ...(since ? { since } : {}) };
|
|
241
259
|
accepted.push({
|
|
242
260
|
eventId: `${opts.tenantId}-generated-${++seq}`,
|
|
243
261
|
eventType: ENERGY_EVENT.generated,
|
package/dist/epcis.d.ts
CHANGED
|
@@ -94,6 +94,35 @@ export declare const CBV_BIZSTEP: {
|
|
|
94
94
|
};
|
|
95
95
|
export type EpcisEventType = 'ObjectEvent' | 'AggregationEvent' | 'TransactionEvent' | 'TransformationEvent';
|
|
96
96
|
export type EpcisAction = 'ADD' | 'OBSERVE' | 'DELETE';
|
|
97
|
+
/**
|
|
98
|
+
* **봉투에 실리는 EPCIS 사건 종류의 이름** — 소비처가 손으로 적지 않게 (2026-08-28).
|
|
99
|
+
*
|
|
100
|
+
* 봉투의 `eventType` 은 `epcis.<클래스>` 다(§`face2-adapter`·§`flow-engine` 이 그 접두사를 붙인다).
|
|
101
|
+
* 그 문자열을 화면·조회가 손으로 적으면 곧 방언이 되고, 클래스가 늘 때 한쪽만 고쳐진다.
|
|
102
|
+
*
|
|
103
|
+
* ── 왜 필요한가 ─────────────────────────────────────────────────────────────
|
|
104
|
+
* 「물건이 움직인 사건만 보는 목록」이 지금 **제외 목록**으로 만들어져 있다 — 운영·에너지 종류 열아홉
|
|
105
|
+
* 개를 적어 빼는 방식이다. 그 방식은 두 가지로 약하다.
|
|
106
|
+
*
|
|
107
|
+
* ① **닫히지 않는다.** 커널이 세 번째 계열을 만들면 그 계열 전부가 오류 없이 「움직임」으로 들어온다.
|
|
108
|
+
* 실제로 그 일이 세 번 있었다(사람 상태·자산 상태·에너지).
|
|
109
|
+
* ② **색인을 못 쓴다.** 제외 + 시각순은 색인이 듣지 않아, 그 트윈의 종류 분포가 비용을 정한다
|
|
110
|
+
* (실측: 에너지가 0.008%인 트윈에서 같은 모양이 37초였다).
|
|
111
|
+
*
|
|
112
|
+
* 담을 것을 말하면 정의가 닫힌다 — **물건이 움직인 사건 = EPCIS 사건**이고 그 클래스는 표준이 정한다.
|
|
113
|
+
* 커널이 채널을 늘려도 이 집합은 늘지 않는다.
|
|
114
|
+
*
|
|
115
|
+
* 우리 커널이 다루는 클래스는 넷이다. 표준의 `AssociationEvent` 는 아직 만들지 않았고, 만들면 이 상수에
|
|
116
|
+
* 더한다 — 그 한 곳만 고치면 소비처가 함께 따라온다.
|
|
117
|
+
*/
|
|
118
|
+
export declare const EPCIS_EVENT: {
|
|
119
|
+
readonly object: "epcis.ObjectEvent";
|
|
120
|
+
readonly aggregation: "epcis.AggregationEvent";
|
|
121
|
+
readonly transaction: "epcis.TransactionEvent";
|
|
122
|
+
readonly transformation: "epcis.TransformationEvent";
|
|
123
|
+
};
|
|
124
|
+
/** 봉투의 `eventType` 이 EPCIS 사건인가 — 접두사 하나로 판정한다(클래스가 늘어도 그대로 산다). */
|
|
125
|
+
export declare function isEpcisEventType(eventType: unknown): boolean;
|
|
97
126
|
/**
|
|
98
127
|
* 수량 요소 — **클래스 식별자 + 얼마나**. 표준이 세 경우를 규정한다(EPCIS 2.0 §7.3.3.1).
|
|
99
128
|
*
|
package/dist/epcis.js
CHANGED
|
@@ -101,6 +101,37 @@ export const CBV_BIZSTEP = {
|
|
|
101
101
|
*/
|
|
102
102
|
other: 'urn:epcglobal:cbv:bizstep:other'
|
|
103
103
|
};
|
|
104
|
+
/**
|
|
105
|
+
* **봉투에 실리는 EPCIS 사건 종류의 이름** — 소비처가 손으로 적지 않게 (2026-08-28).
|
|
106
|
+
*
|
|
107
|
+
* 봉투의 `eventType` 은 `epcis.<클래스>` 다(§`face2-adapter`·§`flow-engine` 이 그 접두사를 붙인다).
|
|
108
|
+
* 그 문자열을 화면·조회가 손으로 적으면 곧 방언이 되고, 클래스가 늘 때 한쪽만 고쳐진다.
|
|
109
|
+
*
|
|
110
|
+
* ── 왜 필요한가 ─────────────────────────────────────────────────────────────
|
|
111
|
+
* 「물건이 움직인 사건만 보는 목록」이 지금 **제외 목록**으로 만들어져 있다 — 운영·에너지 종류 열아홉
|
|
112
|
+
* 개를 적어 빼는 방식이다. 그 방식은 두 가지로 약하다.
|
|
113
|
+
*
|
|
114
|
+
* ① **닫히지 않는다.** 커널이 세 번째 계열을 만들면 그 계열 전부가 오류 없이 「움직임」으로 들어온다.
|
|
115
|
+
* 실제로 그 일이 세 번 있었다(사람 상태·자산 상태·에너지).
|
|
116
|
+
* ② **색인을 못 쓴다.** 제외 + 시각순은 색인이 듣지 않아, 그 트윈의 종류 분포가 비용을 정한다
|
|
117
|
+
* (실측: 에너지가 0.008%인 트윈에서 같은 모양이 37초였다).
|
|
118
|
+
*
|
|
119
|
+
* 담을 것을 말하면 정의가 닫힌다 — **물건이 움직인 사건 = EPCIS 사건**이고 그 클래스는 표준이 정한다.
|
|
120
|
+
* 커널이 채널을 늘려도 이 집합은 늘지 않는다.
|
|
121
|
+
*
|
|
122
|
+
* 우리 커널이 다루는 클래스는 넷이다. 표준의 `AssociationEvent` 는 아직 만들지 않았고, 만들면 이 상수에
|
|
123
|
+
* 더한다 — 그 한 곳만 고치면 소비처가 함께 따라온다.
|
|
124
|
+
*/
|
|
125
|
+
export const EPCIS_EVENT = {
|
|
126
|
+
object: 'epcis.ObjectEvent',
|
|
127
|
+
aggregation: 'epcis.AggregationEvent',
|
|
128
|
+
transaction: 'epcis.TransactionEvent',
|
|
129
|
+
transformation: 'epcis.TransformationEvent'
|
|
130
|
+
};
|
|
131
|
+
/** 봉투의 `eventType` 이 EPCIS 사건인가 — 접두사 하나로 판정한다(클래스가 늘어도 그대로 산다). */
|
|
132
|
+
export function isEpcisEventType(eventType) {
|
|
133
|
+
return typeof eventType === 'string' && eventType.startsWith('epcis.');
|
|
134
|
+
}
|
|
104
135
|
// ── GS1 EPC URI 헬퍼 (표준) ────────────────────────────────────────────────
|
|
105
136
|
/** SSCC (물류단위: 팔레트/화물/트레일러) — 결정적 카운터 기반. */
|
|
106
137
|
export function ssccUri(companyPrefix, serial) {
|
package/dist/flow-engine.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TestResult, ISOTime, MaterialQuantity, WorkCalendarEntry, EffectivePeriod, Effectivity, OffCalendarReason, ResourceProperty, ResourceClassDef, MaterialDefinition, Attention, TwinModelDef, CanonicalEnvelope, Command, CommandAck, EventHandler, EquipmentMotion, OeeMetrics, AssetState, GeneratorSpec, InterventionOutcome, OrderState, PersonState, ScenarioControl, ScenarioOverride, StateSnapshot, TwinKernel, Unsubscribe, LocationState, ItemState, EquipmentState, OrderStatusDelta, TaskState, TaskStatus, StructureShift, IdentityGroundingView, IdentityDeclaration, TestSpecificationCriterion, LocationObservation } from './contract.ts';
|
|
2
|
+
import type { VocabularyElement } from './master-data.ts';
|
|
2
3
|
import type { ReducerCheckpoint } from './observed-reducer.ts';
|
|
3
4
|
import type { EpcisEvent, BizTransactionElement } from './epcis.ts';
|
|
4
5
|
import type { AllocationPolicy, SlotView } from './allocation-policy.ts';
|
|
@@ -964,6 +965,16 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
964
965
|
*
|
|
965
966
|
* `tick` 과 섞어 쓰지 않는다 — 섞으면 무엇이 진실인지 알 수 없다(관측이 시뮬을 덮어쓴다).
|
|
966
967
|
*/
|
|
968
|
+
/**
|
|
969
|
+
* **마스터데이터를 받는다** — 변하지 않는 속성을 상태에 세운다(§`master-data`).
|
|
970
|
+
*
|
|
971
|
+
* 사건이 아니다. 그래서 구독자에게 흘리지 않고(저널에 적히지 않게) 상태만 세운다. 유통기한·로트번호가
|
|
972
|
+
* 이 문으로 온다 — 그 값이 갈 자리가 없어서 전량을 읽는 원본은 「들어오지 않은 것을 들어왔다」고
|
|
973
|
+
* 말해야 했다.
|
|
974
|
+
*
|
|
975
|
+
* 관측 모드가 아니면 아무 일도 하지 않는다(시뮬은 자기 상태를 자기가 만든다) — 그 사실을 수로 낸다.
|
|
976
|
+
*/
|
|
977
|
+
applyMasterData(elements: readonly VocabularyElement[]): number;
|
|
967
978
|
apply(envelope: CanonicalEnvelope): void;
|
|
968
979
|
/** 관측분을 커널 상태로 옮긴다 — 필요할 때 한 번만(같은 규칙, 같은 씨앗 경로). */
|
|
969
980
|
private settleObserved;
|
package/dist/flow-engine.js
CHANGED
|
@@ -2094,6 +2094,27 @@ export class FlowEngine {
|
|
|
2094
2094
|
*
|
|
2095
2095
|
* `tick` 과 섞어 쓰지 않는다 — 섞으면 무엇이 진실인지 알 수 없다(관측이 시뮬을 덮어쓴다).
|
|
2096
2096
|
*/
|
|
2097
|
+
/**
|
|
2098
|
+
* **마스터데이터를 받는다** — 변하지 않는 속성을 상태에 세운다(§`master-data`).
|
|
2099
|
+
*
|
|
2100
|
+
* 사건이 아니다. 그래서 구독자에게 흘리지 않고(저널에 적히지 않게) 상태만 세운다. 유통기한·로트번호가
|
|
2101
|
+
* 이 문으로 온다 — 그 값이 갈 자리가 없어서 전량을 읽는 원본은 「들어오지 않은 것을 들어왔다」고
|
|
2102
|
+
* 말해야 했다.
|
|
2103
|
+
*
|
|
2104
|
+
* 관측 모드가 아니면 아무 일도 하지 않는다(시뮬은 자기 상태를 자기가 만든다) — 그 사실을 수로 낸다.
|
|
2105
|
+
*/
|
|
2106
|
+
applyMasterData(elements) {
|
|
2107
|
+
if (!elements.length)
|
|
2108
|
+
return 0;
|
|
2109
|
+
if (!this.observer) {
|
|
2110
|
+
this.observer = new ObservedReducer(this.boardDef ?? { locations: [], equipment: [] });
|
|
2111
|
+
this.observeMode = true;
|
|
2112
|
+
}
|
|
2113
|
+
const n = this.observer.applyMasterData(elements);
|
|
2114
|
+
/* 상태가 바뀌었으므로 다음 스냅샷·fork 에서 옮겨져야 한다 — 관측 반영과 같은 규율이다. */
|
|
2115
|
+
this.observedDirty = true;
|
|
2116
|
+
return n;
|
|
2117
|
+
}
|
|
2097
2118
|
apply(envelope) {
|
|
2098
2119
|
if (!this.observer) {
|
|
2099
2120
|
this.observer = new ObservedReducer(this.boardDef ?? { locations: [], equipment: [] });
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export * from './counterfactual.ts';
|
|
|
5
5
|
export * from './forecast.ts';
|
|
6
6
|
export * from './twin-observer.ts';
|
|
7
7
|
export * from './event-journal.ts';
|
|
8
|
+
export * from './master-data.ts';
|
|
8
9
|
export * from './divergence.ts';
|
|
9
10
|
export * from './epcis.ts';
|
|
10
11
|
export * from './wms-profile.ts';
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export * from "./counterfactual.js";
|
|
|
5
5
|
export * from "./forecast.js";
|
|
6
6
|
export * from "./twin-observer.js";
|
|
7
7
|
export * from "./event-journal.js";
|
|
8
|
+
export * from "./master-data.js";
|
|
8
9
|
export * from "./divergence.js";
|
|
9
10
|
export * from "./epcis.js";
|
|
10
11
|
export * from "./wms-profile.js";
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** 표준 어휘 타입 — 이 세 가지가 우리 상태 축에 닿는다. 그 밖은 보관하고 알린다. */
|
|
2
|
+
export declare const VOCABULARY_TYPE: {
|
|
3
|
+
/** 품목·로트 클래스의 속성 — 유통기한·로트번호가 여기 온다. */
|
|
4
|
+
readonly epcClass: "urn:epcglobal:epcis:vtype:EPCClass";
|
|
5
|
+
/** 관측 지점의 속성. */
|
|
6
|
+
readonly readPoint: "urn:epcglobal:epcis:vtype:ReadPoint";
|
|
7
|
+
/** 업무 자리의 속성. */
|
|
8
|
+
readonly bizLocation: "urn:epcglobal:epcis:vtype:BizLocation";
|
|
9
|
+
};
|
|
10
|
+
export type VocabularyType = (typeof VOCABULARY_TYPE)[keyof typeof VOCABULARY_TYPE];
|
|
11
|
+
/** 어휘 요소 하나 — 표준의 모양. */
|
|
12
|
+
export interface VocabularyElement {
|
|
13
|
+
/** 그 어휘에서의 식별자(클래스 식별자·자리 식별자). */
|
|
14
|
+
id: string;
|
|
15
|
+
/** 속성 — 이름을 해석하지 않고 그대로 든다. */
|
|
16
|
+
attributes: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 원본이 보내는 마스터데이터 한 건 — 어휘 타입과 함께 온다.
|
|
20
|
+
*
|
|
21
|
+
* 타입을 **받는다.** 로트 전용 문을 만들지 않는 이유는 자리·품목이 같은 통로로 들어와야 하고, 표준이
|
|
22
|
+
* 이미 타입으로 갈라 두었기 때문이다.
|
|
23
|
+
*/
|
|
24
|
+
export interface MasterDataRecord extends VocabularyElement {
|
|
25
|
+
vocabularyType: string;
|
|
26
|
+
}
|
|
27
|
+
/** 이 레코드가 마스터데이터인가 — 갈래 판정은 **커널이 한 곳에서** 한다(어휘 넷을 가르는 방식과 같다). */
|
|
28
|
+
export declare function isMasterDataRecord(record: unknown): record is MasterDataRecord;
|
|
29
|
+
export interface MasterDataResult {
|
|
30
|
+
accepted: MasterDataRecord[];
|
|
31
|
+
rejected: {
|
|
32
|
+
record: unknown;
|
|
33
|
+
errors: string[];
|
|
34
|
+
}[];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 마스터데이터를 받아들인다 — **거부한 것은 이유와 함께 남는다**(다른 어휘와 같은 규율).
|
|
38
|
+
*
|
|
39
|
+
* 빈 속성은 거부한다: 「이 로트에 대해 아무것도 모른다」를 보내는 것은 아무 뜻이 없고, 받아 두면
|
|
40
|
+
* 기존 값을 지우려는 것인지 아무 말도 아닌지 구별할 수 없다.
|
|
41
|
+
*/
|
|
42
|
+
export declare function ingestMasterData(records: MasterDataRecord | MasterDataRecord[] | undefined | null): MasterDataResult;
|
|
43
|
+
/**
|
|
44
|
+
* 클래스 단위 마스터 표 — 관측 리듀서가 든다.
|
|
45
|
+
*
|
|
46
|
+
* 물품은 자리마다 따로 서지만(`클래스@자리`) 이 속성은 **클래스의 것**이다. 그래서 물품마다 복사해
|
|
47
|
+
* 두지 않고 클래스로 한 번 들고, 물품을 세울 때 본다. 마스터가 관측보다 늦게 와도 값이 서는 이유다.
|
|
48
|
+
*/
|
|
49
|
+
export declare class ClassMasterTable {
|
|
50
|
+
private byId;
|
|
51
|
+
/**
|
|
52
|
+
* 속성을 **합친다** — 덮지 않는다(한 번에 일부만 오는 원본이 있다). 바뀐 식별자를 돌려준다.
|
|
53
|
+
*
|
|
54
|
+
* ── 지우는 법 ───────────────────────────────────────────────────────────────
|
|
55
|
+
* 값에 `null` 을 실으면 **그 속성을 지운다**(「모름으로 되돌린다」). 합치기만 하면 원본에서 지운 값이
|
|
56
|
+
* 트윈에 영원히 남고, 그것을 표현할 방법이 없으면 「지웠다」와 「이번엔 말하지 않았다」가 같아진다.
|
|
57
|
+
*
|
|
58
|
+
* `undefined` 는 지우지 않는다 — 그것은 「이 진술에 그 속성이 없다」이고, 원본이 일부만 보내는
|
|
59
|
+
* 정상이다. 두 뜻을 갈라 둔다.
|
|
60
|
+
*
|
|
61
|
+
* 속성이 하나도 남지 않으면 그 요소를 표에서 버린다(빈 껍데기를 들고 있지 않는다).
|
|
62
|
+
*/
|
|
63
|
+
merge(elements: readonly VocabularyElement[]): string[];
|
|
64
|
+
/**
|
|
65
|
+
* 그 식별자의 항목을 버린다 — **아무도 가리키지 않게 된 뒤**에만 부른다.
|
|
66
|
+
*
|
|
67
|
+
* 로트는 계속 새로 생기므로 이 표는 스스로 줄지 않는다. 하루 수백 로트면 1년에 수십만 항목이고,
|
|
68
|
+
* 그것을 아무도 지우지 않으면 재기동으로만 줄어든다(그리고 중간 저장본이 그만큼 커진다).
|
|
69
|
+
*/
|
|
70
|
+
forget(id: string): boolean;
|
|
71
|
+
attributesOf(id: string | undefined): Record<string, unknown> | undefined;
|
|
72
|
+
get size(): number;
|
|
73
|
+
serialize(): Record<string, Record<string, unknown>>;
|
|
74
|
+
restore(saved: unknown): void;
|
|
75
|
+
clear(): void;
|
|
76
|
+
}
|
|
77
|
+
/** 마스터 속성에서 유통기한을 읽는다 — 사건의 `ilmd` 와 **같은 이름**을 본다. */
|
|
78
|
+
export declare function expiryFromAttributes(attrs: Record<string, unknown> | undefined): number | undefined;
|
|
79
|
+
/** 마스터 속성에서 로트 번호를 읽는다 — 사건의 `ilmd` 와 같은 이름. */
|
|
80
|
+
export declare function lotFromAttributes(attrs: Record<string, unknown> | undefined): string | undefined;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { ILMD_ATTR } from "./epcis.js";
|
|
2
|
+
/*
|
|
3
|
+
* **마스터데이터 — 변하지 않는 속성이 들어오는 문** (2026-08-28).
|
|
4
|
+
*
|
|
5
|
+
* ── 무엇이 없어서 무엇이 망가졌나 ───────────────────────────────────────────
|
|
6
|
+
* 커널에 마스터데이터라는 개념이 없었다. 그래서 유통기한 같은 값이 갈 곳이 **사건밖에** 없었고,
|
|
7
|
+
* 표준은 그 값을 `ObjectEvent(action=ADD)` 와 변환 사건에만 실을 수 있게 한다(EPCIS 2.0 §7.3.8).
|
|
8
|
+
*
|
|
9
|
+
* 그 제약이 유입 쪽에서 이렇게 나타났다. 전량을 읽는 원본(식품 제조 MES)이 재고를 다시 말할 때마다,
|
|
10
|
+
* 유통기한을 실으려고 **들어오지 않은 것을 `ADD` 라고** 말했다. 그리고 `ilmd` 는 사건 단위라서 자리별로
|
|
11
|
+
* 묶을 수도 없어 낱개로 나갔다 — 재기동 한 번에 2,665건이다.
|
|
12
|
+
*
|
|
13
|
+
* 나르는 방법의 제약이 사실의 뜻을 정한 것이고, 순서가 거꾸로였다.
|
|
14
|
+
*
|
|
15
|
+
* ── 표준이 둔 자리 ──────────────────────────────────────────────────────────
|
|
16
|
+
* 표준은 이런 값을 **어휘 요소의 속성**으로 담는 자리를 사건과 별개로 갖고 있다.
|
|
17
|
+
*
|
|
18
|
+
* 어휘 타입 urn:epcglobal:epcis:vtype:EPCClass · ReadPoint · BizLocation
|
|
19
|
+
* 어휘 요소 { id, attributes }
|
|
20
|
+
*
|
|
21
|
+
* 속성 **이름**은 EPCIS 본문이 정하지 않고 상위 문서(CBV 마스터데이터)의 몫이다(§7.3.8 이 미룬다).
|
|
22
|
+
* 그래서 이 모듈은 이름을 해석하지 않고 **그대로 보관한다** — 아는 이름만 상태 축으로 옮기고, 모르는
|
|
23
|
+
* 것은 잃지 않는다. 우리가 생산할 때 쓰는 이름은 `ILMD_ATTR` 한 곳에 모여 있고 사건 경로와 같은
|
|
24
|
+
* 상수다(이름이 바뀌면 한 곳만 고친다).
|
|
25
|
+
*
|
|
26
|
+
* ── 무엇을 이 문으로 보내지 않나 ────────────────────────────────────────────
|
|
27
|
+
* **변하는 값은 사건이다.** 수량·위치·상태·진행은 이 문으로 오지 않는다. 여기 오는 것은 그 물건의
|
|
28
|
+
* 생애 동안 같은 값뿐이고, 그래서 「그때는 얼마였나」라는 물음이 성립하지 않는다 — 시각축이 없는 값이다.
|
|
29
|
+
*
|
|
30
|
+
* 그 성질이 「지난 기록에 적지 않는다」의 근거다. 사건에서 파생되는 상태가 아니라 **선언된 것**이므로
|
|
31
|
+
* 모델·자재 선언과 같은 자리에 산다. 다만 중간 저장본에는 담는다(되세울 때 원본을 다시 읽지 않게).
|
|
32
|
+
*
|
|
33
|
+
* 설계: `operato-twin/design/plans/master-data.md`
|
|
34
|
+
*/
|
|
35
|
+
/** 표준 어휘 타입 — 이 세 가지가 우리 상태 축에 닿는다. 그 밖은 보관하고 알린다. */
|
|
36
|
+
export const VOCABULARY_TYPE = {
|
|
37
|
+
/** 품목·로트 클래스의 속성 — 유통기한·로트번호가 여기 온다. */
|
|
38
|
+
epcClass: 'urn:epcglobal:epcis:vtype:EPCClass',
|
|
39
|
+
/** 관측 지점의 속성. */
|
|
40
|
+
readPoint: 'urn:epcglobal:epcis:vtype:ReadPoint',
|
|
41
|
+
/** 업무 자리의 속성. */
|
|
42
|
+
bizLocation: 'urn:epcglobal:epcis:vtype:BizLocation'
|
|
43
|
+
};
|
|
44
|
+
/** 이 레코드가 마스터데이터인가 — 갈래 판정은 **커널이 한 곳에서** 한다(어휘 넷을 가르는 방식과 같다). */
|
|
45
|
+
export function isMasterDataRecord(record) {
|
|
46
|
+
if (!record || typeof record !== 'object')
|
|
47
|
+
return false;
|
|
48
|
+
const r = record;
|
|
49
|
+
return typeof r.vocabularyType === 'string' && !!r.vocabularyType && typeof r.id === 'string' && !!r.id && isPlainObject(r.attributes);
|
|
50
|
+
}
|
|
51
|
+
function isPlainObject(v) {
|
|
52
|
+
return !!v && typeof v === 'object' && !Array.isArray(v);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 마스터데이터를 받아들인다 — **거부한 것은 이유와 함께 남는다**(다른 어휘와 같은 규율).
|
|
56
|
+
*
|
|
57
|
+
* 빈 속성은 거부한다: 「이 로트에 대해 아무것도 모른다」를 보내는 것은 아무 뜻이 없고, 받아 두면
|
|
58
|
+
* 기존 값을 지우려는 것인지 아무 말도 아닌지 구별할 수 없다.
|
|
59
|
+
*/
|
|
60
|
+
export function ingestMasterData(records) {
|
|
61
|
+
const arr = Array.isArray(records) ? records : records ? [records] : [];
|
|
62
|
+
const accepted = [];
|
|
63
|
+
const rejected = [];
|
|
64
|
+
for (const r of arr) {
|
|
65
|
+
const errors = [];
|
|
66
|
+
if (!isMasterDataRecord(r))
|
|
67
|
+
errors.push('vocabularyType · id · attributes 가 있어야 한다');
|
|
68
|
+
else if (!Object.keys(r.attributes).length)
|
|
69
|
+
errors.push('attributes 가 비었다 — 아무 뜻이 없는 진술이다');
|
|
70
|
+
/* 값이 `null` 인 속성은 「지운다」는 뜻이므로 빈 진술이 아니다(위 검사가 이름 수를 세므로 통과한다). */
|
|
71
|
+
if (errors.length)
|
|
72
|
+
rejected.push({ record: r, errors });
|
|
73
|
+
else
|
|
74
|
+
accepted.push({ vocabularyType: r.vocabularyType, id: r.id, attributes: { ...r.attributes } });
|
|
75
|
+
}
|
|
76
|
+
return { accepted, rejected };
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 클래스 단위 마스터 표 — 관측 리듀서가 든다.
|
|
80
|
+
*
|
|
81
|
+
* 물품은 자리마다 따로 서지만(`클래스@자리`) 이 속성은 **클래스의 것**이다. 그래서 물품마다 복사해
|
|
82
|
+
* 두지 않고 클래스로 한 번 들고, 물품을 세울 때 본다. 마스터가 관측보다 늦게 와도 값이 서는 이유다.
|
|
83
|
+
*/
|
|
84
|
+
export class ClassMasterTable {
|
|
85
|
+
byId = new Map();
|
|
86
|
+
/**
|
|
87
|
+
* 속성을 **합친다** — 덮지 않는다(한 번에 일부만 오는 원본이 있다). 바뀐 식별자를 돌려준다.
|
|
88
|
+
*
|
|
89
|
+
* ── 지우는 법 ───────────────────────────────────────────────────────────────
|
|
90
|
+
* 값에 `null` 을 실으면 **그 속성을 지운다**(「모름으로 되돌린다」). 합치기만 하면 원본에서 지운 값이
|
|
91
|
+
* 트윈에 영원히 남고, 그것을 표현할 방법이 없으면 「지웠다」와 「이번엔 말하지 않았다」가 같아진다.
|
|
92
|
+
*
|
|
93
|
+
* `undefined` 는 지우지 않는다 — 그것은 「이 진술에 그 속성이 없다」이고, 원본이 일부만 보내는
|
|
94
|
+
* 정상이다. 두 뜻을 갈라 둔다.
|
|
95
|
+
*
|
|
96
|
+
* 속성이 하나도 남지 않으면 그 요소를 표에서 버린다(빈 껍데기를 들고 있지 않는다).
|
|
97
|
+
*/
|
|
98
|
+
merge(elements) {
|
|
99
|
+
const touched = [];
|
|
100
|
+
for (const e of elements) {
|
|
101
|
+
const next = { ...(this.byId.get(e.id) ?? {}) };
|
|
102
|
+
for (const [name, value] of Object.entries(e.attributes)) {
|
|
103
|
+
if (value === null)
|
|
104
|
+
delete next[name];
|
|
105
|
+
else if (value !== undefined)
|
|
106
|
+
next[name] = value;
|
|
107
|
+
}
|
|
108
|
+
if (Object.keys(next).length)
|
|
109
|
+
this.byId.set(e.id, next);
|
|
110
|
+
else
|
|
111
|
+
this.byId.delete(e.id);
|
|
112
|
+
touched.push(e.id);
|
|
113
|
+
}
|
|
114
|
+
return touched;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* 그 식별자의 항목을 버린다 — **아무도 가리키지 않게 된 뒤**에만 부른다.
|
|
118
|
+
*
|
|
119
|
+
* 로트는 계속 새로 생기므로 이 표는 스스로 줄지 않는다. 하루 수백 로트면 1년에 수십만 항목이고,
|
|
120
|
+
* 그것을 아무도 지우지 않으면 재기동으로만 줄어든다(그리고 중간 저장본이 그만큼 커진다).
|
|
121
|
+
*/
|
|
122
|
+
forget(id) {
|
|
123
|
+
return this.byId.delete(id);
|
|
124
|
+
}
|
|
125
|
+
attributesOf(id) {
|
|
126
|
+
return id ? this.byId.get(id) : undefined;
|
|
127
|
+
}
|
|
128
|
+
get size() {
|
|
129
|
+
return this.byId.size;
|
|
130
|
+
}
|
|
131
|
+
serialize() {
|
|
132
|
+
return Object.fromEntries(this.byId);
|
|
133
|
+
}
|
|
134
|
+
restore(saved) {
|
|
135
|
+
this.byId.clear();
|
|
136
|
+
if (!isPlainObject(saved))
|
|
137
|
+
return;
|
|
138
|
+
for (const [id, attrs] of Object.entries(saved))
|
|
139
|
+
if (isPlainObject(attrs))
|
|
140
|
+
this.byId.set(id, { ...attrs });
|
|
141
|
+
}
|
|
142
|
+
clear() {
|
|
143
|
+
this.byId.clear();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** 마스터 속성에서 유통기한을 읽는다 — 사건의 `ilmd` 와 **같은 이름**을 본다. */
|
|
147
|
+
export function expiryFromAttributes(attrs) {
|
|
148
|
+
return readEpochMs(attrs?.[ILMD_ATTR.expiry]);
|
|
149
|
+
}
|
|
150
|
+
/** 마스터 속성에서 로트 번호를 읽는다 — 사건의 `ilmd` 와 같은 이름. */
|
|
151
|
+
export function lotFromAttributes(attrs) {
|
|
152
|
+
const v = attrs?.[ILMD_ATTR.lot];
|
|
153
|
+
return typeof v === 'string' && v.trim() ? v.trim() : undefined;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* 시각 값을 밀리초로 — 숫자와 문자열을 모두 읽는다(원본이 둘 다 쓴다).
|
|
157
|
+
*
|
|
158
|
+
* 읽을 수 없으면 `undefined` 다. 「지금」으로 메우면 기한이 지난 재고가 멀쩡해 보이고, 0 으로 메우면
|
|
159
|
+
* 멀쩡한 재고가 기한 지난 것으로 보인다 — 둘 다 사실이 아니다.
|
|
160
|
+
*/
|
|
161
|
+
function readEpochMs(raw) {
|
|
162
|
+
if (typeof raw === 'number')
|
|
163
|
+
return Number.isFinite(raw) ? raw : undefined;
|
|
164
|
+
if (typeof raw !== 'string' || !raw.trim())
|
|
165
|
+
return undefined;
|
|
166
|
+
const at = Date.parse(raw);
|
|
167
|
+
return Number.isNaN(at) ? undefined : at;
|
|
168
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AssetState, TestResult, MaterialQuantity, TwinModelDef, CanonicalEnvelope, LocationState, ItemState, LocationObservation, EquipmentState, PersonState, TaskState, OrderState, StructureShift } from './contract.ts';
|
|
2
|
+
import { type VocabularyElement } from './master-data.ts';
|
|
2
3
|
interface ProjItem {
|
|
3
4
|
epc: string;
|
|
4
5
|
/** 로트의 부분(표준 MaterialSubLot.ID) — 비직렬 로트가 자리마다 갈릴 때만. */
|
|
@@ -180,6 +181,13 @@ export interface ReducerCheckpoint {
|
|
|
180
181
|
* 공통 코드로 새어 든다. 그대로 담아 두고 되돌려 줄 뿐이다.
|
|
181
182
|
*/
|
|
182
183
|
domain?: unknown;
|
|
184
|
+
/**
|
|
185
|
+
* **클래스 단위 마스터데이터** — 변하지 않는 속성(유통기한·로트번호)이 사는 자리(§`master-data`).
|
|
186
|
+
*
|
|
187
|
+
* 지난 기록에 적지 않는 값이므로 여기 담지 않으면 되세울 때 빈다. 변하지 않는 값이라 어느 시점의
|
|
188
|
+
* 저장본에서 꺼내도 같은 답이다.
|
|
189
|
+
*/
|
|
190
|
+
classMaster?: Record<string, Record<string, unknown>>;
|
|
183
191
|
}
|
|
184
192
|
export declare class ObservedReducer {
|
|
185
193
|
/** 로케이션 마스터 — 출처를 함께 들고 있다(마스터가 말한 자리 vs 관측으로 알게 된 자리). */
|
|
@@ -198,6 +206,13 @@ export declare class ObservedReducer {
|
|
|
198
206
|
*/
|
|
199
207
|
private heard;
|
|
200
208
|
private items;
|
|
209
|
+
/**
|
|
210
|
+
* **클래스 단위 마스터 표** — 변하지 않는 속성이 사는 자리(§`master-data`).
|
|
211
|
+
*
|
|
212
|
+
* 물품은 자리마다 따로 서지만(`클래스@자리`) 유통기한·로트번호는 **클래스의 것**이다. 그래서 물품마다
|
|
213
|
+
* 복사해 두지 않고 클래스로 한 번 들고, 물품을 세울 때 본다 — 마스터가 관측보다 늦게 와도 값이 선다.
|
|
214
|
+
*/
|
|
215
|
+
private classMaster;
|
|
201
216
|
private aggregation;
|
|
202
217
|
/** 아직 관측되지 않은 자식의 담김 — 물품을 지어내지 않고 보류했다가 등장할 때 붙인다. */
|
|
203
218
|
private pendingParent;
|
|
@@ -346,6 +361,24 @@ export declare class ObservedReducer {
|
|
|
346
361
|
* 표준이 속성 이름을 정의하지 않으므로 모르는 이름은 해석하지 않는다(추측하지 않는다). 원문은
|
|
347
362
|
* `ilmd` 로 그대로 남으니 도메인이 자기 어휘로 읽을 수 있다.
|
|
348
363
|
*/
|
|
364
|
+
/**
|
|
365
|
+
* 이 물품에 해당하는 마스터 속성 — **클래스로 찾고, 없으면 물품 자신의 식별자로** 찾는다.
|
|
366
|
+
*
|
|
367
|
+
* 비직렬 재고는 물품의 키가 곧 클래스 식별자다. 직렬 개체는 클래스가 따로 있다. 두 모양이 다 오므로
|
|
368
|
+
* 둘 다 본다 — 한쪽만 보면 그 원본에서는 값이 조용히 비어 있게 된다.
|
|
369
|
+
*/
|
|
370
|
+
private masterAttributesOf;
|
|
371
|
+
/**
|
|
372
|
+
* **마스터데이터를 받는다** — 어휘 요소의 속성을 상태에 세운다(§`master-data`).
|
|
373
|
+
*
|
|
374
|
+
* 받은 것을 표에 합치고, **그 배치에 든 식별자의 물품만** 다시 세운다. 전부를 훑지 않는 이유는
|
|
375
|
+
* 재고가 수만 건일 수 있고 마스터는 자주 오기 때문이다.
|
|
376
|
+
*
|
|
377
|
+
* 이 값은 지난 기록에 적지 않는다 — 변하지 않는 값이므로 시각축이 없다. 호스트가 그 갈래를 정한다.
|
|
378
|
+
*/
|
|
379
|
+
applyMasterData(elements: readonly VocabularyElement[]): number;
|
|
380
|
+
/** 지금 든 마스터 항목 수 — 화면이 「무엇을 알고 있나」를 말할 수 있게. */
|
|
381
|
+
get masterDataSize(): number;
|
|
349
382
|
private expiryOf;
|
|
350
383
|
private mergeItem;
|
|
351
384
|
/** 이탈(DELETE) — 아이템 + 조립 자식(재귀) 제거. 화물 SSCC DELETE 시 팔레트도 함께 이탈. */
|
package/dist/observed-reducer.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { OP_EVENT, capabilityOf, itemKeyOf, judgeAgainstSpec, requiredTestsFor, locationStatusOf, readBoardEquipment, readBoardLocations, readBoardAssets, effectivityAt, offCalendarAt, offCalendarReasonAt, activeShiftAt } from "./contract.js";
|
|
23
23
|
import { ILMD_ATTR, parseEpc } from "./epcis.js";
|
|
24
|
+
import { ClassMasterTable, expiryFromAttributes, lotFromAttributes } from "./master-data.js";
|
|
24
25
|
/**
|
|
25
26
|
* 투영이 들고 있는 물품 — **계약(ItemState)을 축소하지 않는다.**
|
|
26
27
|
*
|
|
@@ -55,6 +56,13 @@ export class ObservedReducer {
|
|
|
55
56
|
*/
|
|
56
57
|
heard = new Map();
|
|
57
58
|
items = new Map();
|
|
59
|
+
/**
|
|
60
|
+
* **클래스 단위 마스터 표** — 변하지 않는 속성이 사는 자리(§`master-data`).
|
|
61
|
+
*
|
|
62
|
+
* 물품은 자리마다 따로 서지만(`클래스@자리`) 유통기한·로트번호는 **클래스의 것**이다. 그래서 물품마다
|
|
63
|
+
* 복사해 두지 않고 클래스로 한 번 들고, 물품을 세울 때 본다 — 마스터가 관측보다 늦게 와도 값이 선다.
|
|
64
|
+
*/
|
|
65
|
+
classMaster = new ClassMasterTable();
|
|
58
66
|
aggregation = new Map();
|
|
59
67
|
/** 아직 관측되지 않은 자식의 담김 — 물품을 지어내지 않고 보류했다가 등장할 때 붙인다. */
|
|
60
68
|
pendingParent = new Map(); // 자식 EPC → 부모(물류단위)
|
|
@@ -838,6 +846,56 @@ export class ObservedReducer {
|
|
|
838
846
|
* 표준이 속성 이름을 정의하지 않으므로 모르는 이름은 해석하지 않는다(추측하지 않는다). 원문은
|
|
839
847
|
* `ilmd` 로 그대로 남으니 도메인이 자기 어휘로 읽을 수 있다.
|
|
840
848
|
*/
|
|
849
|
+
/**
|
|
850
|
+
* 이 물품에 해당하는 마스터 속성 — **클래스로 찾고, 없으면 물품 자신의 식별자로** 찾는다.
|
|
851
|
+
*
|
|
852
|
+
* 비직렬 재고는 물품의 키가 곧 클래스 식별자다. 직렬 개체는 클래스가 따로 있다. 두 모양이 다 오므로
|
|
853
|
+
* 둘 다 본다 — 한쪽만 보면 그 원본에서는 값이 조용히 비어 있게 된다.
|
|
854
|
+
*/
|
|
855
|
+
masterAttributesOf(classUri, epc) {
|
|
856
|
+
return this.classMaster.attributesOf(classUri) ?? this.classMaster.attributesOf(epc);
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* **마스터데이터를 받는다** — 어휘 요소의 속성을 상태에 세운다(§`master-data`).
|
|
860
|
+
*
|
|
861
|
+
* 받은 것을 표에 합치고, **그 배치에 든 식별자의 물품만** 다시 세운다. 전부를 훑지 않는 이유는
|
|
862
|
+
* 재고가 수만 건일 수 있고 마스터는 자주 오기 때문이다.
|
|
863
|
+
*
|
|
864
|
+
* 이 값은 지난 기록에 적지 않는다 — 변하지 않는 값이므로 시각축이 없다. 호스트가 그 갈래를 정한다.
|
|
865
|
+
*/
|
|
866
|
+
applyMasterData(elements) {
|
|
867
|
+
if (!elements.length)
|
|
868
|
+
return 0;
|
|
869
|
+
const touched = new Set(this.classMaster.merge(elements));
|
|
870
|
+
for (const [key, item] of this.items) {
|
|
871
|
+
/* 클래스로 든 것과 물품 자신의 식별자, 둘 중 하나가 이 배치에 들었으면 다시 세운다. */
|
|
872
|
+
if (!touched.has(item.gtin ?? '') && !touched.has(item.epc))
|
|
873
|
+
continue;
|
|
874
|
+
const attrs = this.masterAttributesOf(item.gtin, item.epc);
|
|
875
|
+
/*
|
|
876
|
+
* **물품이 설 때와 같은 순서로** 세운다(§`mergeItem`).
|
|
877
|
+
*
|
|
878
|
+
* 식별자에서 뽑은 것 → 사건의 `ilmd` → 마스터 → 기존값
|
|
879
|
+
*
|
|
880
|
+
* 여기서 「기존값이 마스터를 이긴다」로 적었더니 값이 **들어온 순서에 따라 달라졌다**: 마스터가
|
|
881
|
+
* 먼저 오면 마스터 값, 늦게 오면 옛 값. 같은 사실에 두 답이 생기는 자리다.
|
|
882
|
+
*/
|
|
883
|
+
const fromId = parseEpc(item.gtin ?? item.epc).lot ?? parseEpc(item.epc).lot;
|
|
884
|
+
this.items.set(key, {
|
|
885
|
+
...item,
|
|
886
|
+
expiry: this.expiryOf(item.ilmd) ?? expiryFromAttributes(attrs) ?? item.expiry,
|
|
887
|
+
lot: fromId ??
|
|
888
|
+
(typeof item.ilmd?.[ILMD_ATTR.lot] === 'string' ? item.ilmd[ILMD_ATTR.lot] : undefined) ??
|
|
889
|
+
lotFromAttributes(attrs) ??
|
|
890
|
+
item.lot
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
return elements.length;
|
|
894
|
+
}
|
|
895
|
+
/** 지금 든 마스터 항목 수 — 화면이 「무엇을 알고 있나」를 말할 수 있게. */
|
|
896
|
+
get masterDataSize() {
|
|
897
|
+
return this.classMaster.size;
|
|
898
|
+
}
|
|
841
899
|
expiryOf(ilmd) {
|
|
842
900
|
const raw = ilmd?.[ILMD_ATTR.expiry];
|
|
843
901
|
if (typeof raw === 'number' && Number.isFinite(raw))
|
|
@@ -894,14 +952,50 @@ export class ObservedReducer {
|
|
|
894
952
|
})(),
|
|
895
953
|
/* 마스터데이터는 생겨날 때 한 번 정해진다 — 뒤 이벤트가 지우지 않게 기존 값을 남긴다. */
|
|
896
954
|
ilmd: patch.ilmd ?? cur?.ilmd,
|
|
897
|
-
|
|
955
|
+
/*
|
|
956
|
+
* ── 보는 순서 (2026-08-28) ─────────────────────────────────────────────
|
|
957
|
+
* ① 사건에 실려 온 `ilmd` 표준의 생성 시점 진술 — 가장 강하다
|
|
958
|
+
* ② 그 클래스의 마스터 표 마스터데이터 통로로 들어온 것(§`master-data`)
|
|
959
|
+
* ③ 이미 갖고 있던 값 뒤 사건이 지우지 않는다
|
|
960
|
+
*
|
|
961
|
+
* ②가 없던 동안, 전량을 읽는 원본은 유통기한을 실으려고 **들어오지 않은 것을 `ADD`** 라고
|
|
962
|
+
* 말해야 했다. 그 값이 갈 자리를 만든 것이 이 줄이다.
|
|
963
|
+
*/
|
|
964
|
+
expiry: this.expiryOf(patch.ilmd) ?? expiryFromAttributes(this.masterAttributesOf(classUri, epc)) ?? cur?.expiry,
|
|
898
965
|
/* 로트는 LGTIN(식별자)에서 오지만, 직렬 개체는 마스터데이터에 실려 온다. */
|
|
899
|
-
lot: parsedClass?.lot ??
|
|
966
|
+
lot: parsedClass?.lot ??
|
|
967
|
+
parsedSelf.lot ??
|
|
968
|
+
(typeof patch.ilmd?.[ILMD_ATTR.lot] === 'string' ? patch.ilmd[ILMD_ATTR.lot] : undefined) ??
|
|
969
|
+
lotFromAttributes(this.masterAttributesOf(classUri, epc)) ??
|
|
970
|
+
cur?.lot
|
|
900
971
|
};
|
|
901
972
|
}
|
|
902
973
|
/** 이탈(DELETE) — 아이템 + 조립 자식(재귀) 제거. 화물 SSCC DELETE 시 팔레트도 함께 이탈. */
|
|
903
974
|
remove(epc) {
|
|
975
|
+
const gone = this.items.get(epc);
|
|
904
976
|
this.items.delete(epc);
|
|
977
|
+
/*
|
|
978
|
+
* ── 마스터 표도 함께 줄인다 (2026-08-28) ───────────────────────────────────
|
|
979
|
+
*
|
|
980
|
+
* 로트는 계속 새로 생기므로 이 표는 스스로 줄지 않는다. 하루 수백 로트면 1년에 수십만 항목이고,
|
|
981
|
+
* 중간 저장본이 그만큼 커진다. 그래서 **아무 물품도 가리키지 않게 된 식별자**를 버린다.
|
|
982
|
+
*
|
|
983
|
+
* 이탈이 일어난 자리에서만 검사한다. 마스터가 먼저 오고 물품이 아직 없는 것은 **정상**이므로
|
|
984
|
+
* (순서를 보장할 수 없다) 그때 「가리키는 물품이 없다」로 지우면 방금 받은 값을 버린다.
|
|
985
|
+
*/
|
|
986
|
+
for (const id of [gone?.gtin, epc]) {
|
|
987
|
+
if (!id || !this.classMaster.attributesOf(id))
|
|
988
|
+
continue;
|
|
989
|
+
let stillUsed = false;
|
|
990
|
+
for (const it of this.items.values()) {
|
|
991
|
+
if (it.gtin === id || it.epc === id) {
|
|
992
|
+
stillUsed = true;
|
|
993
|
+
break;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
if (!stillUsed)
|
|
997
|
+
this.classMaster.forget(id);
|
|
998
|
+
}
|
|
905
999
|
const children = this.aggregation.get(epc);
|
|
906
1000
|
if (children) {
|
|
907
1001
|
this.aggregation.delete(epc);
|
|
@@ -1024,8 +1118,13 @@ export class ObservedReducer {
|
|
|
1024
1118
|
acked: [...this.acked],
|
|
1025
1119
|
corrections: this.corrections.map(c => ({ ...c })),
|
|
1026
1120
|
unhandled: [...this.unhandled.entries()].map(([eventType, v]) => ({ eventType, ...v })),
|
|
1027
|
-
/* 마지막으로 들은 시각 — 이어받지 않으면 재기동 뒤에
|
|
1028
|
-
heard: [...this.heard.entries()].map(([id, v]) => ({ id, ...v }))
|
|
1121
|
+
/* 마지막으로 들은 시각 — 이어받지 않으면 재기동 뒤에 아무 말이 없던 길이를 말할 수 없다. */
|
|
1122
|
+
heard: [...this.heard.entries()].map(([id, v]) => ({ id, ...v })),
|
|
1123
|
+
/*
|
|
1124
|
+
* 마스터데이터도 담는다 — 지난 기록에 없는 값이므로, 담지 않으면 되세울 때 유통기한이 빈다.
|
|
1125
|
+
* 변하지 않는 값이라 어느 시점의 저장본에서 꺼내도 같은 답이다.
|
|
1126
|
+
*/
|
|
1127
|
+
classMaster: this.classMaster.serialize()
|
|
1029
1128
|
};
|
|
1030
1129
|
}
|
|
1031
1130
|
/**
|
|
@@ -1039,6 +1138,7 @@ export class ObservedReducer {
|
|
|
1039
1138
|
this.master = new Map((cp?.master ?? []).map(n => [n.id, { ...n }]));
|
|
1040
1139
|
this.heard = new Map((cp?.heard ?? []).map(h => [h.id, { atMs: h.atMs, ...(h.longestGapMs !== undefined ? { longestGapMs: h.longestGapMs } : {}) }]));
|
|
1041
1140
|
this.items = new Map((cp?.items ?? []).map(i => [i.epc, { ...i }]));
|
|
1141
|
+
this.classMaster.restore(cp?.classMaster);
|
|
1042
1142
|
this.aggregation = new Map((cp?.aggregation ?? []).map(a => [a.parent, [...a.children]]));
|
|
1043
1143
|
this.pendingParent = new Map((cp?.pendingParent ?? []).map(x => [x.child, x.parent]));
|
|
1044
1144
|
this.qtyAggregation = new Map((cp?.qtyAggregation ?? []).map(x => [x.parent, x.quantities]));
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -29,6 +29,7 @@ __export(index_exports, {
|
|
|
29
29
|
CAPABILITY_KEYS: () => CAPABILITY_KEYS,
|
|
30
30
|
CBV_BIZSTEP: () => CBV_BIZSTEP,
|
|
31
31
|
CMD: () => CMD,
|
|
32
|
+
ClassMasterTable: () => ClassMasterTable,
|
|
32
33
|
DEMAND_WINDOW_MS: () => DEMAND_WINDOW_MS,
|
|
33
34
|
DISP: () => DISP,
|
|
34
35
|
DOMAIN_CATALOG: () => DOMAIN_CATALOG,
|
|
@@ -40,6 +41,7 @@ __export(index_exports, {
|
|
|
40
41
|
EMS_TYPES: () => EMS_TYPES,
|
|
41
42
|
ENERGY_EVENT: () => ENERGY_EVENT,
|
|
42
43
|
EPCIS_CONTEXT: () => EPCIS_CONTEXT,
|
|
44
|
+
EPCIS_EVENT: () => EPCIS_EVENT,
|
|
43
45
|
EQUIPMENT_LEVEL: () => EQUIPMENT_LEVEL,
|
|
44
46
|
EmsKernel: () => EmsKernel,
|
|
45
47
|
EventJournal: () => EventJournal,
|
|
@@ -70,6 +72,7 @@ __export(index_exports, {
|
|
|
70
72
|
TwinRuntime: () => TwinRuntime,
|
|
71
73
|
UTC_OFFSET: () => UTC_OFFSET,
|
|
72
74
|
VOCABULARY_EXCEPTIONS: () => VOCABULARY_EXCEPTIONS,
|
|
75
|
+
VOCABULARY_TYPE: () => VOCABULARY_TYPE,
|
|
73
76
|
WMS_LOCATION_TYPES: () => WMS_LOCATION_TYPES,
|
|
74
77
|
WMS_TYPES: () => WMS_TYPES,
|
|
75
78
|
WmsKernel: () => WmsKernel,
|
|
@@ -107,6 +110,7 @@ __export(index_exports, {
|
|
|
107
110
|
electricityCost: () => electricityCost,
|
|
108
111
|
energyIntensity: () => energyIntensity,
|
|
109
112
|
energyOfWindows: () => energyOfWindows,
|
|
113
|
+
expiryFromAttributes: () => expiryFromAttributes,
|
|
110
114
|
fefoPolicy: () => fefoPolicy,
|
|
111
115
|
firstFitPolicy: () => firstFitPolicy,
|
|
112
116
|
foldJobResponses: () => foldJobResponses,
|
|
@@ -123,13 +127,16 @@ __export(index_exports, {
|
|
|
123
127
|
ingestEnergyEquipmentRecords: () => ingestEnergyEquipmentRecords,
|
|
124
128
|
ingestEnergyGenerationRecords: () => ingestEnergyGenerationRecords,
|
|
125
129
|
ingestEnergyRecords: () => ingestEnergyRecords,
|
|
130
|
+
ingestMasterData: () => ingestMasterData,
|
|
126
131
|
ingestOperationalRecords: () => ingestOperationalRecords,
|
|
127
132
|
isAggregationRecord: () => isAggregationRecord,
|
|
128
133
|
isElectricalLocationType: () => isElectricalLocationType,
|
|
129
134
|
isEnergyEquipmentRecord: () => isEnergyEquipmentRecord,
|
|
130
135
|
isEnergyGenerationRecord: () => isEnergyGenerationRecord,
|
|
131
136
|
isEnergyRecord: () => isEnergyRecord,
|
|
137
|
+
isEpcisEventType: () => isEpcisEventType,
|
|
132
138
|
isEquipmentLevel: () => isEquipmentLevel,
|
|
139
|
+
isMasterDataRecord: () => isMasterDataRecord,
|
|
133
140
|
isOperationalRecord: () => isOperationalRecord,
|
|
134
141
|
isOrderTerminal: () => isOrderTerminal,
|
|
135
142
|
isTransformationRecord: () => isTransformationRecord,
|
|
@@ -140,6 +147,7 @@ __export(index_exports, {
|
|
|
140
147
|
levelOfLocationType: () => levelOfLocationType,
|
|
141
148
|
lgtinClass: () => lgtinClass,
|
|
142
149
|
locationStatusOf: () => locationStatusOf,
|
|
150
|
+
lotFromAttributes: () => lotFromAttributes,
|
|
143
151
|
mapRecord: () => mapRecord,
|
|
144
152
|
mapRecordChecked: () => mapRecordChecked,
|
|
145
153
|
meetsTests: () => meetsTests,
|
|
@@ -1115,6 +1123,15 @@ var CBV_BIZSTEP = {
|
|
|
1115
1123
|
*/
|
|
1116
1124
|
other: "urn:epcglobal:cbv:bizstep:other"
|
|
1117
1125
|
};
|
|
1126
|
+
var EPCIS_EVENT = {
|
|
1127
|
+
object: "epcis.ObjectEvent",
|
|
1128
|
+
aggregation: "epcis.AggregationEvent",
|
|
1129
|
+
transaction: "epcis.TransactionEvent",
|
|
1130
|
+
transformation: "epcis.TransformationEvent"
|
|
1131
|
+
};
|
|
1132
|
+
function isEpcisEventType(eventType) {
|
|
1133
|
+
return typeof eventType === "string" && eventType.startsWith("epcis.");
|
|
1134
|
+
}
|
|
1118
1135
|
function ssccUri(companyPrefix, serial) {
|
|
1119
1136
|
return `urn:epc:id:sscc:${companyPrefix}.${String(serial).padStart(10, "0")}`;
|
|
1120
1137
|
}
|
|
@@ -1380,6 +1397,105 @@ function validateEpcisEvent(e) {
|
|
|
1380
1397
|
return v;
|
|
1381
1398
|
}
|
|
1382
1399
|
|
|
1400
|
+
// src/master-data.ts
|
|
1401
|
+
var VOCABULARY_TYPE = {
|
|
1402
|
+
/** 품목·로트 클래스의 속성 — 유통기한·로트번호가 여기 온다. */
|
|
1403
|
+
epcClass: "urn:epcglobal:epcis:vtype:EPCClass",
|
|
1404
|
+
/** 관측 지점의 속성. */
|
|
1405
|
+
readPoint: "urn:epcglobal:epcis:vtype:ReadPoint",
|
|
1406
|
+
/** 업무 자리의 속성. */
|
|
1407
|
+
bizLocation: "urn:epcglobal:epcis:vtype:BizLocation"
|
|
1408
|
+
};
|
|
1409
|
+
function isMasterDataRecord(record) {
|
|
1410
|
+
if (!record || typeof record !== "object") return false;
|
|
1411
|
+
const r = record;
|
|
1412
|
+
return typeof r.vocabularyType === "string" && !!r.vocabularyType && typeof r.id === "string" && !!r.id && isPlainObject(r.attributes);
|
|
1413
|
+
}
|
|
1414
|
+
function isPlainObject(v) {
|
|
1415
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
1416
|
+
}
|
|
1417
|
+
function ingestMasterData(records) {
|
|
1418
|
+
const arr = Array.isArray(records) ? records : records ? [records] : [];
|
|
1419
|
+
const accepted = [];
|
|
1420
|
+
const rejected = [];
|
|
1421
|
+
for (const r of arr) {
|
|
1422
|
+
const errors = [];
|
|
1423
|
+
if (!isMasterDataRecord(r)) errors.push("vocabularyType \xB7 id \xB7 attributes \uAC00 \uC788\uC5B4\uC57C \uD55C\uB2E4");
|
|
1424
|
+
else if (!Object.keys(r.attributes).length) errors.push("attributes \uAC00 \uBE44\uC5C8\uB2E4 \u2014 \uC544\uBB34 \uB73B\uC774 \uC5C6\uB294 \uC9C4\uC220\uC774\uB2E4");
|
|
1425
|
+
if (errors.length) rejected.push({ record: r, errors });
|
|
1426
|
+
else accepted.push({ vocabularyType: r.vocabularyType, id: r.id, attributes: { ...r.attributes } });
|
|
1427
|
+
}
|
|
1428
|
+
return { accepted, rejected };
|
|
1429
|
+
}
|
|
1430
|
+
var ClassMasterTable = class {
|
|
1431
|
+
byId = /* @__PURE__ */ new Map();
|
|
1432
|
+
/**
|
|
1433
|
+
* 속성을 **합친다** — 덮지 않는다(한 번에 일부만 오는 원본이 있다). 바뀐 식별자를 돌려준다.
|
|
1434
|
+
*
|
|
1435
|
+
* ── 지우는 법 ───────────────────────────────────────────────────────────────
|
|
1436
|
+
* 값에 `null` 을 실으면 **그 속성을 지운다**(「모름으로 되돌린다」). 합치기만 하면 원본에서 지운 값이
|
|
1437
|
+
* 트윈에 영원히 남고, 그것을 표현할 방법이 없으면 「지웠다」와 「이번엔 말하지 않았다」가 같아진다.
|
|
1438
|
+
*
|
|
1439
|
+
* `undefined` 는 지우지 않는다 — 그것은 「이 진술에 그 속성이 없다」이고, 원본이 일부만 보내는
|
|
1440
|
+
* 정상이다. 두 뜻을 갈라 둔다.
|
|
1441
|
+
*
|
|
1442
|
+
* 속성이 하나도 남지 않으면 그 요소를 표에서 버린다(빈 껍데기를 들고 있지 않는다).
|
|
1443
|
+
*/
|
|
1444
|
+
merge(elements) {
|
|
1445
|
+
const touched = [];
|
|
1446
|
+
for (const e of elements) {
|
|
1447
|
+
const next = { ...this.byId.get(e.id) ?? {} };
|
|
1448
|
+
for (const [name, value] of Object.entries(e.attributes)) {
|
|
1449
|
+
if (value === null) delete next[name];
|
|
1450
|
+
else if (value !== void 0) next[name] = value;
|
|
1451
|
+
}
|
|
1452
|
+
if (Object.keys(next).length) this.byId.set(e.id, next);
|
|
1453
|
+
else this.byId.delete(e.id);
|
|
1454
|
+
touched.push(e.id);
|
|
1455
|
+
}
|
|
1456
|
+
return touched;
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* 그 식별자의 항목을 버린다 — **아무도 가리키지 않게 된 뒤**에만 부른다.
|
|
1460
|
+
*
|
|
1461
|
+
* 로트는 계속 새로 생기므로 이 표는 스스로 줄지 않는다. 하루 수백 로트면 1년에 수십만 항목이고,
|
|
1462
|
+
* 그것을 아무도 지우지 않으면 재기동으로만 줄어든다(그리고 중간 저장본이 그만큼 커진다).
|
|
1463
|
+
*/
|
|
1464
|
+
forget(id) {
|
|
1465
|
+
return this.byId.delete(id);
|
|
1466
|
+
}
|
|
1467
|
+
attributesOf(id) {
|
|
1468
|
+
return id ? this.byId.get(id) : void 0;
|
|
1469
|
+
}
|
|
1470
|
+
get size() {
|
|
1471
|
+
return this.byId.size;
|
|
1472
|
+
}
|
|
1473
|
+
serialize() {
|
|
1474
|
+
return Object.fromEntries(this.byId);
|
|
1475
|
+
}
|
|
1476
|
+
restore(saved) {
|
|
1477
|
+
this.byId.clear();
|
|
1478
|
+
if (!isPlainObject(saved)) return;
|
|
1479
|
+
for (const [id, attrs] of Object.entries(saved)) if (isPlainObject(attrs)) this.byId.set(id, { ...attrs });
|
|
1480
|
+
}
|
|
1481
|
+
clear() {
|
|
1482
|
+
this.byId.clear();
|
|
1483
|
+
}
|
|
1484
|
+
};
|
|
1485
|
+
function expiryFromAttributes(attrs) {
|
|
1486
|
+
return readEpochMs(attrs?.[ILMD_ATTR.expiry]);
|
|
1487
|
+
}
|
|
1488
|
+
function lotFromAttributes(attrs) {
|
|
1489
|
+
const v = attrs?.[ILMD_ATTR.lot];
|
|
1490
|
+
return typeof v === "string" && v.trim() ? v.trim() : void 0;
|
|
1491
|
+
}
|
|
1492
|
+
function readEpochMs(raw) {
|
|
1493
|
+
if (typeof raw === "number") return Number.isFinite(raw) ? raw : void 0;
|
|
1494
|
+
if (typeof raw !== "string" || !raw.trim()) return void 0;
|
|
1495
|
+
const at = Date.parse(raw);
|
|
1496
|
+
return Number.isNaN(at) ? void 0 : at;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1383
1499
|
// src/observed-reducer.ts
|
|
1384
1500
|
var UNKNOWN_TYPE = "unknown";
|
|
1385
1501
|
function effectiveOf(r) {
|
|
@@ -1405,6 +1521,13 @@ var ObservedReducer = class {
|
|
|
1405
1521
|
*/
|
|
1406
1522
|
heard = /* @__PURE__ */ new Map();
|
|
1407
1523
|
items = /* @__PURE__ */ new Map();
|
|
1524
|
+
/**
|
|
1525
|
+
* **클래스 단위 마스터 표** — 변하지 않는 속성이 사는 자리(§`master-data`).
|
|
1526
|
+
*
|
|
1527
|
+
* 물품은 자리마다 따로 서지만(`클래스@자리`) 유통기한·로트번호는 **클래스의 것**이다. 그래서 물품마다
|
|
1528
|
+
* 복사해 두지 않고 클래스로 한 번 들고, 물품을 세울 때 본다 — 마스터가 관측보다 늦게 와도 값이 선다.
|
|
1529
|
+
*/
|
|
1530
|
+
classMaster = new ClassMasterTable();
|
|
1408
1531
|
aggregation = /* @__PURE__ */ new Map();
|
|
1409
1532
|
/** 아직 관측되지 않은 자식의 담김 — 물품을 지어내지 않고 보류했다가 등장할 때 붙인다. */
|
|
1410
1533
|
pendingParent = /* @__PURE__ */ new Map();
|
|
@@ -1953,6 +2076,42 @@ var ObservedReducer = class {
|
|
|
1953
2076
|
* 표준이 속성 이름을 정의하지 않으므로 모르는 이름은 해석하지 않는다(추측하지 않는다). 원문은
|
|
1954
2077
|
* `ilmd` 로 그대로 남으니 도메인이 자기 어휘로 읽을 수 있다.
|
|
1955
2078
|
*/
|
|
2079
|
+
/**
|
|
2080
|
+
* 이 물품에 해당하는 마스터 속성 — **클래스로 찾고, 없으면 물품 자신의 식별자로** 찾는다.
|
|
2081
|
+
*
|
|
2082
|
+
* 비직렬 재고는 물품의 키가 곧 클래스 식별자다. 직렬 개체는 클래스가 따로 있다. 두 모양이 다 오므로
|
|
2083
|
+
* 둘 다 본다 — 한쪽만 보면 그 원본에서는 값이 조용히 비어 있게 된다.
|
|
2084
|
+
*/
|
|
2085
|
+
masterAttributesOf(classUri, epc) {
|
|
2086
|
+
return this.classMaster.attributesOf(classUri) ?? this.classMaster.attributesOf(epc);
|
|
2087
|
+
}
|
|
2088
|
+
/**
|
|
2089
|
+
* **마스터데이터를 받는다** — 어휘 요소의 속성을 상태에 세운다(§`master-data`).
|
|
2090
|
+
*
|
|
2091
|
+
* 받은 것을 표에 합치고, **그 배치에 든 식별자의 물품만** 다시 세운다. 전부를 훑지 않는 이유는
|
|
2092
|
+
* 재고가 수만 건일 수 있고 마스터는 자주 오기 때문이다.
|
|
2093
|
+
*
|
|
2094
|
+
* 이 값은 지난 기록에 적지 않는다 — 변하지 않는 값이므로 시각축이 없다. 호스트가 그 갈래를 정한다.
|
|
2095
|
+
*/
|
|
2096
|
+
applyMasterData(elements) {
|
|
2097
|
+
if (!elements.length) return 0;
|
|
2098
|
+
const touched = new Set(this.classMaster.merge(elements));
|
|
2099
|
+
for (const [key, item] of this.items) {
|
|
2100
|
+
if (!touched.has(item.gtin ?? "") && !touched.has(item.epc)) continue;
|
|
2101
|
+
const attrs = this.masterAttributesOf(item.gtin, item.epc);
|
|
2102
|
+
const fromId = parseEpc(item.gtin ?? item.epc).lot ?? parseEpc(item.epc).lot;
|
|
2103
|
+
this.items.set(key, {
|
|
2104
|
+
...item,
|
|
2105
|
+
expiry: this.expiryOf(item.ilmd) ?? expiryFromAttributes(attrs) ?? item.expiry,
|
|
2106
|
+
lot: fromId ?? (typeof item.ilmd?.[ILMD_ATTR.lot] === "string" ? item.ilmd[ILMD_ATTR.lot] : void 0) ?? lotFromAttributes(attrs) ?? item.lot
|
|
2107
|
+
});
|
|
2108
|
+
}
|
|
2109
|
+
return elements.length;
|
|
2110
|
+
}
|
|
2111
|
+
/** 지금 든 마스터 항목 수 — 화면이 「무엇을 알고 있나」를 말할 수 있게. */
|
|
2112
|
+
get masterDataSize() {
|
|
2113
|
+
return this.classMaster.size;
|
|
2114
|
+
}
|
|
1956
2115
|
expiryOf(ilmd) {
|
|
1957
2116
|
const raw = ilmd?.[ILMD_ATTR.expiry];
|
|
1958
2117
|
if (typeof raw === "number" && Number.isFinite(raw)) return raw;
|
|
@@ -1997,14 +2156,35 @@ var ObservedReducer = class {
|
|
|
1997
2156
|
})(),
|
|
1998
2157
|
/* 마스터데이터는 생겨날 때 한 번 정해진다 — 뒤 이벤트가 지우지 않게 기존 값을 남긴다. */
|
|
1999
2158
|
ilmd: patch.ilmd ?? cur?.ilmd,
|
|
2000
|
-
|
|
2159
|
+
/*
|
|
2160
|
+
* ── 보는 순서 (2026-08-28) ─────────────────────────────────────────────
|
|
2161
|
+
* ① 사건에 실려 온 `ilmd` 표준의 생성 시점 진술 — 가장 강하다
|
|
2162
|
+
* ② 그 클래스의 마스터 표 마스터데이터 통로로 들어온 것(§`master-data`)
|
|
2163
|
+
* ③ 이미 갖고 있던 값 뒤 사건이 지우지 않는다
|
|
2164
|
+
*
|
|
2165
|
+
* ②가 없던 동안, 전량을 읽는 원본은 유통기한을 실으려고 **들어오지 않은 것을 `ADD`** 라고
|
|
2166
|
+
* 말해야 했다. 그 값이 갈 자리를 만든 것이 이 줄이다.
|
|
2167
|
+
*/
|
|
2168
|
+
expiry: this.expiryOf(patch.ilmd) ?? expiryFromAttributes(this.masterAttributesOf(classUri, epc)) ?? cur?.expiry,
|
|
2001
2169
|
/* 로트는 LGTIN(식별자)에서 오지만, 직렬 개체는 마스터데이터에 실려 온다. */
|
|
2002
|
-
lot: parsedClass?.lot ?? parsedSelf.lot ?? (typeof patch.ilmd?.[ILMD_ATTR.lot] === "string" ? patch.ilmd[ILMD_ATTR.lot] : void 0) ?? cur?.lot
|
|
2170
|
+
lot: parsedClass?.lot ?? parsedSelf.lot ?? (typeof patch.ilmd?.[ILMD_ATTR.lot] === "string" ? patch.ilmd[ILMD_ATTR.lot] : void 0) ?? lotFromAttributes(this.masterAttributesOf(classUri, epc)) ?? cur?.lot
|
|
2003
2171
|
};
|
|
2004
2172
|
}
|
|
2005
2173
|
/** 이탈(DELETE) — 아이템 + 조립 자식(재귀) 제거. 화물 SSCC DELETE 시 팔레트도 함께 이탈. */
|
|
2006
2174
|
remove(epc) {
|
|
2175
|
+
const gone = this.items.get(epc);
|
|
2007
2176
|
this.items.delete(epc);
|
|
2177
|
+
for (const id of [gone?.gtin, epc]) {
|
|
2178
|
+
if (!id || !this.classMaster.attributesOf(id)) continue;
|
|
2179
|
+
let stillUsed = false;
|
|
2180
|
+
for (const it of this.items.values()) {
|
|
2181
|
+
if (it.gtin === id || it.epc === id) {
|
|
2182
|
+
stillUsed = true;
|
|
2183
|
+
break;
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
if (!stillUsed) this.classMaster.forget(id);
|
|
2187
|
+
}
|
|
2008
2188
|
const children = this.aggregation.get(epc);
|
|
2009
2189
|
if (children) {
|
|
2010
2190
|
this.aggregation.delete(epc);
|
|
@@ -2122,8 +2302,13 @@ var ObservedReducer = class {
|
|
|
2122
2302
|
acked: [...this.acked],
|
|
2123
2303
|
corrections: this.corrections.map((c) => ({ ...c })),
|
|
2124
2304
|
unhandled: [...this.unhandled.entries()].map(([eventType, v]) => ({ eventType, ...v })),
|
|
2125
|
-
/* 마지막으로 들은 시각 — 이어받지 않으면 재기동 뒤에
|
|
2126
|
-
heard: [...this.heard.entries()].map(([id, v]) => ({ id, ...v }))
|
|
2305
|
+
/* 마지막으로 들은 시각 — 이어받지 않으면 재기동 뒤에 아무 말이 없던 길이를 말할 수 없다. */
|
|
2306
|
+
heard: [...this.heard.entries()].map(([id, v]) => ({ id, ...v })),
|
|
2307
|
+
/*
|
|
2308
|
+
* 마스터데이터도 담는다 — 지난 기록에 없는 값이므로, 담지 않으면 되세울 때 유통기한이 빈다.
|
|
2309
|
+
* 변하지 않는 값이라 어느 시점의 저장본에서 꺼내도 같은 답이다.
|
|
2310
|
+
*/
|
|
2311
|
+
classMaster: this.classMaster.serialize()
|
|
2127
2312
|
};
|
|
2128
2313
|
}
|
|
2129
2314
|
/**
|
|
@@ -2139,6 +2324,7 @@ var ObservedReducer = class {
|
|
|
2139
2324
|
this.master = new Map((cp?.master ?? []).map((n) => [n.id, { ...n }]));
|
|
2140
2325
|
this.heard = new Map((cp?.heard ?? []).map((h) => [h.id, { atMs: h.atMs, ...h.longestGapMs !== void 0 ? { longestGapMs: h.longestGapMs } : {} }]));
|
|
2141
2326
|
this.items = new Map((cp?.items ?? []).map((i) => [i.epc, { ...i }]));
|
|
2327
|
+
this.classMaster.restore(cp?.classMaster);
|
|
2142
2328
|
this.aggregation = new Map((cp?.aggregation ?? []).map((a) => [a.parent, [...a.children]]));
|
|
2143
2329
|
this.pendingParent = new Map((cp?.pendingParent ?? []).map((x) => [x.child, x.parent]));
|
|
2144
2330
|
this.qtyAggregation = new Map((cp?.qtyAggregation ?? []).map((x) => [x.parent, x.quantities]));
|
|
@@ -2524,7 +2710,7 @@ var CAPABILITIES = {
|
|
|
2524
2710
|
* 계량 능력(`metered`)이 `kW` 와 `kWh` 를 함께 선언한 것과 같은 짝이다. 적산은 발전 설비의 보편적
|
|
2525
2711
|
* 사실이고(태양광·열병합·디젤), 그것이 없으면 성능비·발전시간 같은 성과를 아무도 셀 수 없다.
|
|
2526
2712
|
*/
|
|
2527
|
-
stateFields: ["generatedKW", "exportKW", "generatedKWh", "generatedKWhResetAt"],
|
|
2713
|
+
stateFields: ["generatedKW", "exportKW", "generatedKWh", "generatedKWhResetAt", "generatedKWhSince", "generatedKWhBasis"],
|
|
2528
2714
|
models: ["GeneratingState"],
|
|
2529
2715
|
results: ["generated"]
|
|
2530
2716
|
},
|
|
@@ -4241,6 +4427,25 @@ var FlowEngine = class {
|
|
|
4241
4427
|
*
|
|
4242
4428
|
* `tick` 과 섞어 쓰지 않는다 — 섞으면 무엇이 진실인지 알 수 없다(관측이 시뮬을 덮어쓴다).
|
|
4243
4429
|
*/
|
|
4430
|
+
/**
|
|
4431
|
+
* **마스터데이터를 받는다** — 변하지 않는 속성을 상태에 세운다(§`master-data`).
|
|
4432
|
+
*
|
|
4433
|
+
* 사건이 아니다. 그래서 구독자에게 흘리지 않고(저널에 적히지 않게) 상태만 세운다. 유통기한·로트번호가
|
|
4434
|
+
* 이 문으로 온다 — 그 값이 갈 자리가 없어서 전량을 읽는 원본은 「들어오지 않은 것을 들어왔다」고
|
|
4435
|
+
* 말해야 했다.
|
|
4436
|
+
*
|
|
4437
|
+
* 관측 모드가 아니면 아무 일도 하지 않는다(시뮬은 자기 상태를 자기가 만든다) — 그 사실을 수로 낸다.
|
|
4438
|
+
*/
|
|
4439
|
+
applyMasterData(elements) {
|
|
4440
|
+
if (!elements.length) return 0;
|
|
4441
|
+
if (!this.observer) {
|
|
4442
|
+
this.observer = new ObservedReducer(this.boardDef ?? { locations: [], equipment: [] });
|
|
4443
|
+
this.observeMode = true;
|
|
4444
|
+
}
|
|
4445
|
+
const n = this.observer.applyMasterData(elements);
|
|
4446
|
+
this.observedDirty = true;
|
|
4447
|
+
return n;
|
|
4448
|
+
}
|
|
4244
4449
|
apply(envelope) {
|
|
4245
4450
|
if (!this.observer) {
|
|
4246
4451
|
this.observer = new ObservedReducer(this.boardDef ?? { locations: [], equipment: [] });
|
|
@@ -7722,11 +7927,15 @@ var EmsKernel = class extends FlowEngine {
|
|
|
7722
7927
|
if (heardAt !== void 0 && atMs < heardAt) return;
|
|
7723
7928
|
this.generationHeardAtMs.set(id, atMs);
|
|
7724
7929
|
const before = Number(eq.generatedKWh);
|
|
7725
|
-
|
|
7930
|
+
const priorSince = eq.generatedKWhSince;
|
|
7931
|
+
const since = typeof d?.since === "string" && d.since.trim() ? d.since.trim() : void 0;
|
|
7932
|
+
const periodAdvanced = !!since && !!priorSince && Date.parse(since) > Date.parse(priorSince);
|
|
7933
|
+
if (Number.isFinite(before) && kWh < before && !periodAdvanced) {
|
|
7726
7934
|
;
|
|
7727
7935
|
eq.generatedKWhResetAt = new Date(atMs).toISOString();
|
|
7936
|
+
eq.generatedKWhBasis = since ? "declared" : "inferred";
|
|
7728
7937
|
}
|
|
7729
|
-
;
|
|
7938
|
+
if (since) eq.generatedKWhSince = since;
|
|
7730
7939
|
eq.generatedKWh = kWh;
|
|
7731
7940
|
eq.measuredAt = new Date(atMs).toISOString();
|
|
7732
7941
|
this.revision++;
|
|
@@ -9246,11 +9455,21 @@ function ingestEnergyGenerationRecords(records, opts) {
|
|
|
9246
9455
|
}
|
|
9247
9456
|
const eventTime = String(r?.at ?? "").trim() || opts.defaultEventTime || "";
|
|
9248
9457
|
if (!eventTime) errors.push("at \uC774 \uC5C6\uACE0 \uAE30\uBCF8 \uC2DC\uAC01\uB3C4 \uC8FC\uC9C0 \uC54A\uC558\uB2E4 \u2014 \uC5B8\uC81C \uC7B0 \uAC83\uC778\uC9C0 \uC9C0\uC5B4\uB0BC \uC218 \uC5C6\uB2E4");
|
|
9458
|
+
let since;
|
|
9459
|
+
const rawSince = r?.kWhSince;
|
|
9460
|
+
if (rawSince !== void 0 && rawSince !== null && String(rawSince).trim()) {
|
|
9461
|
+
const text = String(rawSince).trim();
|
|
9462
|
+
const at = Date.parse(text);
|
|
9463
|
+
if (!Number.isFinite(at)) errors.push(`kWhSince \uB97C \uC2DC\uAC01\uC73C\uB85C \uC77D\uC744 \uC218 \uC5C6\uB2E4: ${JSON.stringify(rawSince)}`);
|
|
9464
|
+
else if (Number.isFinite(Date.parse(eventTime)) && at > Date.parse(eventTime)) {
|
|
9465
|
+
errors.push(`kWhSince(${text}) \uAC00 \uC7B0 \uC2DC\uAC01(${eventTime})\uBCF4\uB2E4 \uB4A4\uB2E4 \u2014 \uB458 \uC911 \uD558\uB098\uAC00 \uD2C0\uB838\uB2E4`);
|
|
9466
|
+
} else since = text;
|
|
9467
|
+
}
|
|
9249
9468
|
if (errors.length) {
|
|
9250
9469
|
rejected.push({ record: r, errors });
|
|
9251
9470
|
continue;
|
|
9252
9471
|
}
|
|
9253
|
-
const data = { equipmentId, kWh, at: eventTime };
|
|
9472
|
+
const data = { equipmentId, kWh, at: eventTime, ...since ? { since } : {} };
|
|
9254
9473
|
accepted.push({
|
|
9255
9474
|
eventId: `${opts.tenantId}-generated-${++seq}`,
|
|
9256
9475
|
eventType: ENERGY_EVENT.generated,
|
|
@@ -9840,6 +10059,7 @@ function retiredVocabularyIn(line) {
|
|
|
9840
10059
|
CAPABILITY_KEYS,
|
|
9841
10060
|
CBV_BIZSTEP,
|
|
9842
10061
|
CMD,
|
|
10062
|
+
ClassMasterTable,
|
|
9843
10063
|
DEMAND_WINDOW_MS,
|
|
9844
10064
|
DISP,
|
|
9845
10065
|
DOMAIN_CATALOG,
|
|
@@ -9851,6 +10071,7 @@ function retiredVocabularyIn(line) {
|
|
|
9851
10071
|
EMS_TYPES,
|
|
9852
10072
|
ENERGY_EVENT,
|
|
9853
10073
|
EPCIS_CONTEXT,
|
|
10074
|
+
EPCIS_EVENT,
|
|
9854
10075
|
EQUIPMENT_LEVEL,
|
|
9855
10076
|
EmsKernel,
|
|
9856
10077
|
EventJournal,
|
|
@@ -9881,6 +10102,7 @@ function retiredVocabularyIn(line) {
|
|
|
9881
10102
|
TwinRuntime,
|
|
9882
10103
|
UTC_OFFSET,
|
|
9883
10104
|
VOCABULARY_EXCEPTIONS,
|
|
10105
|
+
VOCABULARY_TYPE,
|
|
9884
10106
|
WMS_LOCATION_TYPES,
|
|
9885
10107
|
WMS_TYPES,
|
|
9886
10108
|
WmsKernel,
|
|
@@ -9918,6 +10140,7 @@ function retiredVocabularyIn(line) {
|
|
|
9918
10140
|
electricityCost,
|
|
9919
10141
|
energyIntensity,
|
|
9920
10142
|
energyOfWindows,
|
|
10143
|
+
expiryFromAttributes,
|
|
9921
10144
|
fefoPolicy,
|
|
9922
10145
|
firstFitPolicy,
|
|
9923
10146
|
foldJobResponses,
|
|
@@ -9934,13 +10157,16 @@ function retiredVocabularyIn(line) {
|
|
|
9934
10157
|
ingestEnergyEquipmentRecords,
|
|
9935
10158
|
ingestEnergyGenerationRecords,
|
|
9936
10159
|
ingestEnergyRecords,
|
|
10160
|
+
ingestMasterData,
|
|
9937
10161
|
ingestOperationalRecords,
|
|
9938
10162
|
isAggregationRecord,
|
|
9939
10163
|
isElectricalLocationType,
|
|
9940
10164
|
isEnergyEquipmentRecord,
|
|
9941
10165
|
isEnergyGenerationRecord,
|
|
9942
10166
|
isEnergyRecord,
|
|
10167
|
+
isEpcisEventType,
|
|
9943
10168
|
isEquipmentLevel,
|
|
10169
|
+
isMasterDataRecord,
|
|
9944
10170
|
isOperationalRecord,
|
|
9945
10171
|
isOrderTerminal,
|
|
9946
10172
|
isTransformationRecord,
|
|
@@ -9951,6 +10177,7 @@ function retiredVocabularyIn(line) {
|
|
|
9951
10177
|
levelOfLocationType,
|
|
9952
10178
|
lgtinClass,
|
|
9953
10179
|
locationStatusOf,
|
|
10180
|
+
lotFromAttributes,
|
|
9954
10181
|
mapRecord,
|
|
9955
10182
|
mapRecordChecked,
|
|
9956
10183
|
meetsTests,
|
package/package.json
CHANGED