@operato/twin-kernel 0.7.64 → 0.7.66
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 +2 -0
- package/dist/capability.js +9 -1
- package/dist/contract.d.ts +81 -7
- package/dist/contract.js +1 -1
- package/dist/divergence.js +1 -1
- package/dist/domain-catalog.js +1 -1
- package/dist/ems-kernel.d.ts +31 -4
- package/dist/ems-kernel.js +114 -7
- package/dist/ems-profile.d.ts +19 -0
- package/dist/ems-profile.js +42 -0
- package/dist/energy-ingest.d.ts +19 -2
- package/dist/energy-ingest.js +58 -2
- package/dist/epcis.d.ts +2 -2
- package/dist/epcis.js +2 -2
- package/dist/event-journal.d.ts +8 -8
- package/dist/event-journal.js +8 -8
- package/dist/face2-adapter.d.ts +34 -1
- package/dist/face2-adapter.js +40 -0
- package/dist/flow-engine.d.ts +16 -7
- package/dist/flow-engine.js +91 -12
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/job-response.d.ts +1 -1
- package/dist/job-response.js +1 -1
- package/dist/kernel.js +1 -1
- package/dist/mes-kernel.d.ts +1 -1
- package/dist/mes-kernel.js +1 -1
- package/dist/observed-reducer.d.ts +57 -11
- package/dist/observed-reducer.js +76 -13
- package/dist/operational-ingest.d.ts +1 -1
- package/dist/operational-ingest.js +6 -6
- package/dist/state-projector.js +1 -1
- package/dist/task-fold.d.ts +2 -2
- package/dist/task-fold.js +1 -1
- package/dist/yms-kernel.js +1 -1
- package/dist-cjs/index.cjs +297 -30
- package/package.json +1 -1
package/dist-cjs/index.cjs
CHANGED
|
@@ -52,6 +52,7 @@ __export(index_exports, {
|
|
|
52
52
|
MES_BIZSTEP: () => MES_BIZSTEP,
|
|
53
53
|
MES_LOCATION_TYPES: () => MES_LOCATION_TYPES,
|
|
54
54
|
MES_TYPES: () => MES_TYPES,
|
|
55
|
+
METER_DIRECTION: () => METER_DIRECTION,
|
|
55
56
|
MesKernel: () => MesKernel,
|
|
56
57
|
OPERATION_PROPERTY: () => OPERATION_PROPERTY,
|
|
57
58
|
OP_EVENT: () => OP_EVENT,
|
|
@@ -120,11 +121,13 @@ __export(index_exports, {
|
|
|
120
121
|
inWorkCalendarAt: () => inWorkCalendarAt,
|
|
121
122
|
ingest: () => ingest,
|
|
122
123
|
ingestEnergyEquipmentRecords: () => ingestEnergyEquipmentRecords,
|
|
124
|
+
ingestEnergyGenerationRecords: () => ingestEnergyGenerationRecords,
|
|
123
125
|
ingestEnergyRecords: () => ingestEnergyRecords,
|
|
124
126
|
ingestOperationalRecords: () => ingestOperationalRecords,
|
|
125
127
|
isAggregationRecord: () => isAggregationRecord,
|
|
126
128
|
isElectricalLocationType: () => isElectricalLocationType,
|
|
127
129
|
isEnergyEquipmentRecord: () => isEnergyEquipmentRecord,
|
|
130
|
+
isEnergyGenerationRecord: () => isEnergyGenerationRecord,
|
|
128
131
|
isEnergyRecord: () => isEnergyRecord,
|
|
129
132
|
isEquipmentLevel: () => isEquipmentLevel,
|
|
130
133
|
isOperationalRecord: () => isOperationalRecord,
|
|
@@ -1036,12 +1039,12 @@ var DISP = {
|
|
|
1036
1039
|
/**
|
|
1037
1040
|
* **기한이 지났다** — CBV `expired`.
|
|
1038
1041
|
*
|
|
1039
|
-
* ── 왜 `non_sellable` 로
|
|
1042
|
+
* ── 왜 `non_sellable` 로 계산하지 않나 (2026-08-24) ─────────────────────────────
|
|
1040
1043
|
* 커널의 `non_sellable` 은 CBV 의 `non_sellable_other`, 즉 **「그 밖의 이유」**다. 기한 지남을 거기
|
|
1041
1044
|
* 넣으면 「기한이 지나 못 판다」와 「깨져서 못 판다」가 같은 값이 되고, 화면은 회수·폐기의 사유를
|
|
1042
1045
|
* 구별할 수 없다. 식품에서 그 둘은 다른 조치다.
|
|
1043
1046
|
*
|
|
1044
|
-
* 그리고 표준에 **정확한 낱말이 있다** —
|
|
1047
|
+
* 그리고 표준에 **정확한 낱말이 있다** — 합치는 것은 있는 낱말을 버리는 것이다.
|
|
1045
1048
|
*
|
|
1046
1049
|
* ── 기한 날짜와 다른 축이다 ────────────────────────────────────────────────
|
|
1047
1050
|
* `ItemState.expiry` 는 **날짜**이고 이것은 **상태**다. 날짜가 있으면 「지났나」는 파생이지만, 원본이
|
|
@@ -1389,6 +1392,17 @@ var ObservedReducer = class {
|
|
|
1389
1392
|
master = /* @__PURE__ */ new Map();
|
|
1390
1393
|
/** 자리별 · 속성별 **마지막 관측** — 이력이 아니다(§`OP_EVENT.observation`). */
|
|
1391
1394
|
observations = /* @__PURE__ */ new Map();
|
|
1395
|
+
/**
|
|
1396
|
+
* 설비별 **마지막으로 들은 시각** — 그 설비의 상태가 아니라 **우리 지식의 상태**다.
|
|
1397
|
+
*
|
|
1398
|
+
* ── 왜 상태에 넣지 않나 ─────────────────────────────────────────────────────
|
|
1399
|
+
* 「가동 중」은 설비에 대한 사실이고 「그 말을 언제 들었나」는 우리에 대한 사실이다. 상태에 섞으면
|
|
1400
|
+
* 두 종류가 한 통에 담기고, 자체 구동(시뮬)에는 없는 값이라 두 구동이 어긋난다 — 물품의
|
|
1401
|
+
* `seenAtMs` 가 같은 이유로 상태에서 빠졌다(§`snapshot`).
|
|
1402
|
+
*
|
|
1403
|
+
* 그래서 여기 안에 두고, 밖으로는 **주의 신호**로만 낸다(§`equipmentSilence`).
|
|
1404
|
+
*/
|
|
1405
|
+
heard = /* @__PURE__ */ new Map();
|
|
1392
1406
|
items = /* @__PURE__ */ new Map();
|
|
1393
1407
|
aggregation = /* @__PURE__ */ new Map();
|
|
1394
1408
|
/** 아직 관측되지 않은 자식의 담김 — 물품을 지어내지 않고 보류했다가 등장할 때 붙인다. */
|
|
@@ -1447,12 +1461,12 @@ var ObservedReducer = class {
|
|
|
1447
1461
|
/**
|
|
1448
1462
|
* **구조를 전환한다** — 관측된 사실은 지키고 토폴로지만 새 선언으로 바꾼다.
|
|
1449
1463
|
*
|
|
1450
|
-
* 공장은 바뀐다. 도장 부스를 넷 더 놓고,
|
|
1464
|
+
* 공장은 바뀐다. 도장 부스를 넷 더 놓고, 라인 하나를 닫는다. 그런데 지금까지는 구조가 바뀌면
|
|
1451
1465
|
* **그 트윈의 저널을 통째로 지우는 것**이 유일한 길이었다 — 안 지우면 옛 이벤트를 새 공장에 대고
|
|
1452
|
-
*
|
|
1466
|
+
* 계산하게 되어 이력이 거짓말을 한다. 역사를 잃거나 거짓말을 하거나, 둘뿐이었다.
|
|
1453
1467
|
*
|
|
1454
1468
|
* 셋째 길이 이것이다: 이벤트가 **자기 구조를 달고** 다니고, 재생은 구조가 바뀌는 지점에서 여기를
|
|
1455
|
-
* 불러 전환한 뒤
|
|
1469
|
+
* 불러 전환한 뒤 이어서 계산한다. 그러면 "그때 그 공장의 사실" 로 계속 읽힌다.
|
|
1456
1470
|
*
|
|
1457
1471
|
* ── 무엇을 지키고 무엇을 버리는가 ───────────────────────────────────────
|
|
1458
1472
|
* **관측은 지킨다** — 물품·오더·작업·집합은 구조와 무관한 사실이다(팔레트는 부스를 늘려도 그대로다).
|
|
@@ -1566,6 +1580,48 @@ var ObservedReducer = class {
|
|
|
1566
1580
|
this.lastAt.set(key, { at, recorded: Number.isFinite(recorded) ? recorded : seen.recorded });
|
|
1567
1581
|
return false;
|
|
1568
1582
|
}
|
|
1583
|
+
/**
|
|
1584
|
+
* 이 설비의 값을 받은 시각을 적고, 지난번 값과의 간격 중 가장 긴 것을 함께 기록한다.
|
|
1585
|
+
*
|
|
1586
|
+
* 간격을 기록하는 이유는 판단 기준을 우리가 정하지 않기 위해서다. 「몇 분 넘게 소식이 없으면
|
|
1587
|
+
* 이상하다」는 현장마다 다르다 — 1초마다 값을 보내는 계측기와 하루 한 번 올라오는 점검 기록이
|
|
1588
|
+
* 같은 표에 있다. 기준을 하나로 정하면 앞쪽에는 신호가 너무 많이 서고 뒤쪽에는 서지 않는다.
|
|
1589
|
+
* 그래서 그 설비가 지금까지 실제로 보여 준 간격을 기준으로 쓴다.
|
|
1590
|
+
*/
|
|
1591
|
+
markHeard(id, e) {
|
|
1592
|
+
const at = Date.parse(String(e.eventTime ?? ""));
|
|
1593
|
+
if (!Number.isFinite(at)) return;
|
|
1594
|
+
const prev = this.heard.get(id);
|
|
1595
|
+
if (!prev) {
|
|
1596
|
+
this.heard.set(id, { atMs: at });
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
if (at <= prev.atMs) return;
|
|
1600
|
+
const gap = at - prev.atMs;
|
|
1601
|
+
this.heard.set(id, { atMs: at, longestGapMs: Math.max(prev.longestGapMs ?? 0, gap) });
|
|
1602
|
+
}
|
|
1603
|
+
/**
|
|
1604
|
+
* 값이 지난 간격보다 오래 끊긴 설비의 목록. 밖으로 내보내는 값은 이것 하나다.
|
|
1605
|
+
*
|
|
1606
|
+
* 끊긴 시간은 이 트윈이 마지막으로 어떤 값이든 받은 시각을 기준으로 잰다. 연결이 통째로 멈추면 그
|
|
1607
|
+
* 기준도 멈추므로 아무 신호도 서지 않는다. 그때 알려야 할 사실은 설비마다의 상태가 아니라 「연결이
|
|
1608
|
+
* 끊겼다」 하나이고, 그것은 유입 장부가 알린다.
|
|
1609
|
+
*
|
|
1610
|
+
* 지난 간격을 아직 모르는 설비는 목록에 넣지 않는다. 값을 한 번만 받았다면 이 설비가 원래 얼마나
|
|
1611
|
+
* 뜸하게 보내는지 알 수 없다.
|
|
1612
|
+
*/
|
|
1613
|
+
equipmentSilence() {
|
|
1614
|
+
const out = /* @__PURE__ */ new Map();
|
|
1615
|
+
const now = this.observedAtMs;
|
|
1616
|
+
if (now === void 0) return out;
|
|
1617
|
+
for (const [id, h] of this.heard) {
|
|
1618
|
+
if (h.longestGapMs === void 0) continue;
|
|
1619
|
+
const silentMs = now - h.atMs;
|
|
1620
|
+
if (silentMs <= 0) continue;
|
|
1621
|
+
out.set(id, { silentMs, longestBeforeMs: h.longestGapMs });
|
|
1622
|
+
}
|
|
1623
|
+
return out;
|
|
1624
|
+
}
|
|
1569
1625
|
/** 대상별 마지막 반영 시각 — 순서 판정용(대상=EPC·작업·설비·오더 id). */
|
|
1570
1626
|
lastAt = /* @__PURE__ */ new Map();
|
|
1571
1627
|
/**
|
|
@@ -1624,6 +1680,7 @@ var ObservedReducer = class {
|
|
|
1624
1680
|
const known = this.equipment.get(d.moverId);
|
|
1625
1681
|
const homeLocation = d.homeLocation ?? known?.homeLocation;
|
|
1626
1682
|
const eqPeriod = d.effectiveStart || d.effectiveEnd ? effectiveOf(d) : effectiveOf(known);
|
|
1683
|
+
this.markHeard(d.moverId, e);
|
|
1627
1684
|
this.equipment.set(d.moverId, { id: d.moverId, kind: d.kind, status: d.status, location: d.location, ...homeLocation ? { homeLocation } : {}, taskId: d.taskId, motion: d.motion, origin: known?.origin ?? "observed", ...eqPeriod, ...d.held ? { held: true } : {} });
|
|
1628
1685
|
break;
|
|
1629
1686
|
}
|
|
@@ -1974,7 +2031,7 @@ var ObservedReducer = class {
|
|
|
1974
2031
|
* 시뮬 기준시각(BASE_EPOCH, 2026-01-01)에 얼어 있었다.
|
|
1975
2032
|
*
|
|
1976
2033
|
* 결과는 조용했다: 구간 마감·피크 같은 **커널이 낸 사실이 8개월 전으로 기록**되어, 저널을 기간으로
|
|
1977
|
-
*
|
|
2034
|
+
* 계산하는 소비처(성과·요금·피더 배분)가 그 트윈에서 영원히 아무것도 찾지 못했다. 계측 자체는 원천 시각을
|
|
1978
2035
|
* 달고 있었으므로 화면의 순간값만 정상으로 보였다 — 그래서 눈에 띄지 않았다.
|
|
1979
2036
|
*
|
|
1980
2037
|
* 시각을 되돌리지는 않는다: 늦게 도착한 옛 표본이 "지금" 을 과거로 끌면 그 뒤 판정이 전부 흔들린다.
|
|
@@ -2033,20 +2090,20 @@ var ObservedReducer = class {
|
|
|
2033
2090
|
/**
|
|
2034
2091
|
* **재개점(checkpoint)** — 스냅샷과 다르다.
|
|
2035
2092
|
*
|
|
2036
|
-
* ── 왜 스냅샷으로는 이어
|
|
2093
|
+
* ── 왜 스냅샷으로는 이어 계산할 수 없나 (2026-08-18) ─────────────────────────
|
|
2037
2094
|
* `snapshot()` 은 **소비처가 보는 값**이다(파생된 판정·정리된 목록). 그것으로 리듀서를 되세우면
|
|
2038
2095
|
* 보이지 않는 것들이 사라진다 — 아직 등장하지 않은 자식의 담김(`pendingParent`), 집합 관계
|
|
2039
2096
|
* (`aggregation`), 반영 못 한 사건 집계(`unhandled`), 정정 선언(`corrections`). 그 상태에서 뒤 이벤트를
|
|
2040
|
-
*
|
|
2097
|
+
* 계산하면 **0부터 계산한 결과와 달라진다.** 다르면 그 차이는 조용하다(오류가 없다).
|
|
2041
2098
|
*
|
|
2042
|
-
* 그래서 재개점은 **내부 상태 전부**다. 「이어
|
|
2043
|
-
* 0부터
|
|
2099
|
+
* 그래서 재개점은 **내부 상태 전부**다. 「이어 계산」의 정합성은 시험이 증명한다:
|
|
2100
|
+
* 0부터 계산한 결과 == 앞부분 재개점 + 뒷부분 계산.
|
|
2044
2101
|
*
|
|
2045
2102
|
* 모델에서 오는 것(교대·등급·시간대)은 담지 않는다 — 되세울 때 같은 모델을 받기 때문이다.
|
|
2046
2103
|
*/
|
|
2047
2104
|
serialize() {
|
|
2048
2105
|
return {
|
|
2049
|
-
/* 관측도 재개점에 든다 — 없으면 이어
|
|
2106
|
+
/* 관측도 재개점에 든다 — 없으면 이어 계산가 그 축을 0부터 다시 만든다. */
|
|
2050
2107
|
observations: [...this.observations.entries()].map(([id, bin]) => ({ id, values: [...bin.values()] })),
|
|
2051
2108
|
revision: this.revision,
|
|
2052
2109
|
master: [...this.master.values()].map((n) => ({ ...n })),
|
|
@@ -2063,7 +2120,9 @@ var ObservedReducer = class {
|
|
|
2063
2120
|
orders: [...this.orders.values()].map((o) => ({ ...o })),
|
|
2064
2121
|
acked: [...this.acked],
|
|
2065
2122
|
corrections: this.corrections.map((c) => ({ ...c })),
|
|
2066
|
-
unhandled: [...this.unhandled.entries()].map(([eventType, v]) => ({ eventType, ...v }))
|
|
2123
|
+
unhandled: [...this.unhandled.entries()].map(([eventType, v]) => ({ eventType, ...v })),
|
|
2124
|
+
/* 마지막으로 들은 시각 — 이어받지 않으면 재기동 뒤에 침묵의 길이를 말할 수 없다. */
|
|
2125
|
+
heard: [...this.heard.entries()].map(([id, v]) => ({ id, ...v }))
|
|
2067
2126
|
};
|
|
2068
2127
|
}
|
|
2069
2128
|
/**
|
|
@@ -2077,6 +2136,7 @@ var ObservedReducer = class {
|
|
|
2077
2136
|
);
|
|
2078
2137
|
this.revision = cp?.revision ?? 0;
|
|
2079
2138
|
this.master = new Map((cp?.master ?? []).map((n) => [n.id, { ...n }]));
|
|
2139
|
+
this.heard = new Map((cp?.heard ?? []).map((h) => [h.id, { atMs: h.atMs, ...h.longestGapMs !== void 0 ? { longestGapMs: h.longestGapMs } : {} }]));
|
|
2080
2140
|
this.items = new Map((cp?.items ?? []).map((i) => [i.epc, { ...i }]));
|
|
2081
2141
|
this.aggregation = new Map((cp?.aggregation ?? []).map((a) => [a.parent, [...a.children]]));
|
|
2082
2142
|
this.pendingParent = new Map((cp?.pendingParent ?? []).map((x) => [x.child, x.parent]));
|
|
@@ -2225,10 +2285,29 @@ var WMS_TYPES = [
|
|
|
2225
2285
|
|
|
2226
2286
|
// src/ems-profile.ts
|
|
2227
2287
|
var EMS_LOCATION_TYPES = ["incoming", "feeder", "submeter-zone"];
|
|
2288
|
+
var METER_DIRECTION = ["import", "export", "bidirectional"];
|
|
2228
2289
|
var EMS_EQUIPMENT_TYPES = ["meter", "breaker", "pv-array", "battery", "utility", "curtailable-load"];
|
|
2229
2290
|
var EMS_PROPERTY = {
|
|
2230
2291
|
/** 계약전력(kW) — 수전·분기 자리에 선언한다. 없으면 계약 대비 판정을 하지 않는다. */
|
|
2231
2292
|
contractKW: "contract.kW",
|
|
2293
|
+
/*
|
|
2294
|
+
* ── 계량 지점의 방향 (2026-08-25) ──────────────────────────────────────────
|
|
2295
|
+
* **요금은 계통 접속점의 순 유입에 매겨진다.** 그러니 그 지점의 계량기가 무엇을 재는지 모르면
|
|
2296
|
+
* 요금을 사실대로 말할 수 없다 — 내보낸 양을 쓴 양으로 세면 계약 대비 판단이 반대로 뒤집힌다.
|
|
2297
|
+
*
|
|
2298
|
+
* 이 축이 없는 동안 계량 지점의 kW 를 전부 부하로 셌다. 상계 거래를 하는 현장, 축전지를 붙인 현장,
|
|
2299
|
+
* 열병합이 있는 현장 — 접속점 계량기가 양쪽을 재는 곳은 늘 있다.
|
|
2300
|
+
*
|
|
2301
|
+
* **발전기를 계량기로 만드는 것과 다른 일이다.** 발전은 설비의 사실이고(`energyGenerating`), 그것을
|
|
2302
|
+
* 계량 지점으로 보내는 것은 여전히 하지 않는다. 이 축은 **접속점 계량기**가 무엇을 재는지 말하는
|
|
2303
|
+
* 자리다.
|
|
2304
|
+
*
|
|
2305
|
+
* 방향은 **관측이 아니라 선언**이다. 표본마다 바뀌는 값이 아니고, 그 계량기가 무엇을 재도록
|
|
2306
|
+
* 설치되었는지는 현장이 안다. 그래서 표본(`EnergyRecord`)에 넣지 않고 자원 속성으로 둔다.
|
|
2307
|
+
*
|
|
2308
|
+
* 값은 계량기 자신의 낱말을 쓴다(적산 레지스터가 그렇게 갈려 있다). §`METER_DIRECTION`.
|
|
2309
|
+
*/
|
|
2310
|
+
meterDirection: "meter.direction",
|
|
2232
2311
|
/*
|
|
2233
2312
|
* ── 부하 계수 — 시뮬레이션이 전기를 만들 수 있게 (2026-08-14) ───────────────
|
|
2234
2313
|
* 「이 설비가 돌면 몇 kW 인가」는 **현장이 아는 값**이다. 그래서 타입 기본값을 두지 않는다:
|
|
@@ -2314,6 +2393,11 @@ var EMS_PROPERTY = {
|
|
|
2314
2393
|
};
|
|
2315
2394
|
var EMS_PROPERTY_SPEC = {
|
|
2316
2395
|
[EMS_PROPERTY.contractKW]: { uom: "kW", dataType: "xs:double", note: "contracted power at the metering point, in kW." },
|
|
2396
|
+
/* 값이 셋 중 하나여야 한다 — 그 밖의 낱말은 받지 않는다(뜻이 통할 것 같은 말도 받지 않는다). */
|
|
2397
|
+
[EMS_PROPERTY.meterDirection]: {
|
|
2398
|
+
dataType: "xs:string",
|
|
2399
|
+
note: 'what this metering point measures: "import" (drawn from the grid), "export" (generation sent out), or "bidirectional" (one meter for both, sign carries the direction). Left undeclared, the point counts as load \u2014 leaving it out never makes demand look smaller than it is.'
|
|
2400
|
+
},
|
|
2317
2401
|
[EMS_PROPERTY.ratedKW]: { uom: "kW", dataType: "xs:double", note: "power drawn while running, in kW." },
|
|
2318
2402
|
[EMS_PROPERTY.standbyKW]: { uom: "kW", dataType: "xs:double", note: "power drawn while idle, in kW." },
|
|
2319
2403
|
[EMS_PROPERTY.demandChargePerKW]: { dataType: "xs:double", note: "demand charge per kW of billing-period peak, in the declared currency." },
|
|
@@ -2571,7 +2655,15 @@ var CAPABILITIES = {
|
|
|
2571
2655
|
label: "twin.capability.energyGenerating",
|
|
2572
2656
|
semantics: "\uC804\uAE30\uB97C \uB9CC\uB4E0\uB2E4 \u2014 \uC5ED\uC1A1\uC740 \uC18C\uBE44\uC758 \uC74C\uC218\uAC00 \uC544\uB2C8\uB77C \uBCC4\uAC1C \uC0AC\uC2E4\uC774\uB2E4.",
|
|
2573
2657
|
commands: [],
|
|
2574
|
-
|
|
2658
|
+
/*
|
|
2659
|
+
* ── 적산을 더한다 (2026-08-26) ─────────────────────────────────────────────
|
|
2660
|
+
* 이 능력이 순시 출력만 선언하고 있었다. 그래서 「지금 발전 중」은 말하고 「얼마나 발전했나」는 말할
|
|
2661
|
+
* 수 없었다 — 사건 이름(`energy.generated`)과 설계 문서에는 처음부터 있던 사실이다.
|
|
2662
|
+
*
|
|
2663
|
+
* 계량 능력(`metered`)이 `kW` 와 `kWh` 를 함께 선언한 것과 같은 짝이다. 적산은 발전 설비의 보편적
|
|
2664
|
+
* 사실이고(태양광·열병합·디젤), 그것이 없으면 성능비·발전시간 같은 성과를 아무도 셀 수 없다.
|
|
2665
|
+
*/
|
|
2666
|
+
stateFields: ["generatedKW", "exportKW", "generatedKWh", "generatedKWhResetAt"],
|
|
2575
2667
|
models: ["GeneratingState"],
|
|
2576
2668
|
results: ["generated"]
|
|
2577
2669
|
},
|
|
@@ -2990,7 +3082,7 @@ var TWIN_RELATIONS = [
|
|
|
2990
3082
|
{ from: "operations", field: "resourceType", target: { kind: "type", role: "equipment" }, via: "twin.rel.by", optional: true }
|
|
2991
3083
|
];
|
|
2992
3084
|
var TWIN_PROPERTIES = [
|
|
2993
|
-
/* 공정 — 선언(ISO 8601 기간)과 관측(저널에서
|
|
3085
|
+
/* 공정 — 선언(ISO 8601 기간)과 관측(저널에서 계산한 분포)이 둘 다 있는 유일한 짝. */
|
|
2994
3086
|
{
|
|
2995
3087
|
axis: "operations",
|
|
2996
3088
|
key: "duration",
|
|
@@ -3276,12 +3368,30 @@ function resolveObject(v, record, field, errors) {
|
|
|
3276
3368
|
const o = { ...r };
|
|
3277
3369
|
return Object.keys(o).length ? o : void 0;
|
|
3278
3370
|
}
|
|
3371
|
+
function resolveErrorDeclaration(spec, record, errors) {
|
|
3372
|
+
if (!spec) return void 0;
|
|
3373
|
+
const declarationTime = resolve(spec.declarationTime, record, "errorDeclaration.declarationTime", errors);
|
|
3374
|
+
const reason = resolve(spec.reason, record, "errorDeclaration.reason", errors);
|
|
3375
|
+
const correctiveEventIDs = resolveList(spec.correctiveEventIDs, record, "errorDeclaration.correctiveEventIDs", errors);
|
|
3376
|
+
if (!declarationTime) {
|
|
3377
|
+
if (reason || correctiveEventIDs.length) {
|
|
3378
|
+
errors.push("errorDeclaration \uC774 \uBC18\uCBE4 \uC654\uB2E4 \u2014 \uC774\uC720\xB7\uC815\uC815 \uB300\uC0C1\uC740 \uC788\uB294\uB370 declarationTime \uC774 \uC5C6\uB2E4(\uC815\uC815 \uC2DC\uAC01\uC740 \uC9C0\uC5B4\uB0BC \uC218 \uC5C6\uB2E4)");
|
|
3379
|
+
}
|
|
3380
|
+
return void 0;
|
|
3381
|
+
}
|
|
3382
|
+
return {
|
|
3383
|
+
declarationTime,
|
|
3384
|
+
...reason ? { reason } : {},
|
|
3385
|
+
...correctiveEventIDs.length ? { correctiveEventIDs } : {}
|
|
3386
|
+
};
|
|
3387
|
+
}
|
|
3279
3388
|
function mapRecordChecked(record, mapping, eventTime) {
|
|
3280
3389
|
const errors = [];
|
|
3281
3390
|
const bizStep = resolve(mapping.bizStep, record, "bizStep", errors) ?? "";
|
|
3282
3391
|
const disposition = resolve(mapping.disposition, record, "disposition", errors);
|
|
3283
3392
|
const readPoint = resolve(mapping.readPoint, record, "readPoint", errors);
|
|
3284
3393
|
const bizLocation = resolve(mapping.bizLocation, record, "bizLocation", errors);
|
|
3394
|
+
const errorDeclaration = resolveErrorDeclaration(mapping.errorDeclaration, record, errors);
|
|
3285
3395
|
if (mapping.type === "TransformationEvent") {
|
|
3286
3396
|
const inputEPCList = resolveList(mapping.inputEPCList, record, "inputEPCList", errors);
|
|
3287
3397
|
const inputQuantityList = resolveQuantityList(mapping.inputQuantityList, record, "inputQuantityList", errors);
|
|
@@ -3306,6 +3416,7 @@ function mapRecordChecked(record, mapping, eventTime) {
|
|
|
3306
3416
|
...outputEPCList.length ? { outputEPCList } : {},
|
|
3307
3417
|
...outputQuantityList.length ? { outputQuantityList } : {},
|
|
3308
3418
|
...transformationID ? { transformationID } : {},
|
|
3419
|
+
...errorDeclaration ? { errorDeclaration } : {},
|
|
3309
3420
|
readPoint,
|
|
3310
3421
|
bizLocation
|
|
3311
3422
|
}),
|
|
@@ -3327,6 +3438,7 @@ function mapRecordChecked(record, mapping, eventTime) {
|
|
|
3327
3438
|
/* 없는 쪽은 **필드를 만들지 않는다** — 빈 배열을 실으면 「자식이 없다」고 말하는 것이 된다. */
|
|
3328
3439
|
...childEPCs.length ? { childEPCs } : {},
|
|
3329
3440
|
...childQuantityList.length ? { childQuantityList } : {},
|
|
3441
|
+
...errorDeclaration ? { errorDeclaration } : {},
|
|
3330
3442
|
readPoint,
|
|
3331
3443
|
bizLocation
|
|
3332
3444
|
}),
|
|
@@ -3349,6 +3461,7 @@ function mapRecordChecked(record, mapping, eventTime) {
|
|
|
3349
3461
|
epcList: epc ? [epc] : [],
|
|
3350
3462
|
...quantityList.length ? { quantityList } : {},
|
|
3351
3463
|
...ilmd ? { ilmd } : {},
|
|
3464
|
+
...errorDeclaration ? { errorDeclaration } : {},
|
|
3352
3465
|
readPoint,
|
|
3353
3466
|
bizLocation
|
|
3354
3467
|
}),
|
|
@@ -3652,6 +3765,21 @@ function deriveAttentions(view, acked, nowIso, thresholds) {
|
|
|
3652
3765
|
suggestedAction: { code: "act.repair", command: CMD.resourceRepair, args: { resourceId: m.id } }
|
|
3653
3766
|
});
|
|
3654
3767
|
}
|
|
3768
|
+
if (m.silentMs !== void 0 && m.longestSilenceBeforeMs !== void 0 && m.silentMs > m.longestSilenceBeforeMs) {
|
|
3769
|
+
out.push({
|
|
3770
|
+
id: `silent:${m.id}`,
|
|
3771
|
+
kind: "equipment-silent",
|
|
3772
|
+
severity: "medium",
|
|
3773
|
+
anchor: { moverId: m.id, locationId: m.location },
|
|
3774
|
+
params: {
|
|
3775
|
+
moverId: m.id,
|
|
3776
|
+
...m.location ? { locationId: m.location } : {},
|
|
3777
|
+
silentMs: m.silentMs,
|
|
3778
|
+
longestBeforeMs: m.longestSilenceBeforeMs,
|
|
3779
|
+
...m.status ? { lastStatus: m.status } : {}
|
|
3780
|
+
}
|
|
3781
|
+
});
|
|
3782
|
+
}
|
|
3655
3783
|
}
|
|
3656
3784
|
for (const n of view.locations) {
|
|
3657
3785
|
for (const c of n.criteria ?? []) {
|
|
@@ -3812,10 +3940,14 @@ function computeOee(c, nowMs) {
|
|
|
3812
3940
|
scrapCount: c.scrapCount
|
|
3813
3941
|
};
|
|
3814
3942
|
}
|
|
3943
|
+
var ENERGY_STATE_FIELDS = [
|
|
3944
|
+
...stateFieldsOf(["metered", "curtailable", "energyGenerating", "energyStoring", "switching"]),
|
|
3945
|
+
"measuredAt"
|
|
3946
|
+
];
|
|
3815
3947
|
function energyFieldsOf(m) {
|
|
3816
3948
|
const out = {};
|
|
3817
3949
|
const src = m;
|
|
3818
|
-
for (const k of
|
|
3950
|
+
for (const k of ENERGY_STATE_FIELDS) {
|
|
3819
3951
|
if (src[k] !== void 0) out[k] = src[k];
|
|
3820
3952
|
}
|
|
3821
3953
|
return out;
|
|
@@ -4038,7 +4170,7 @@ var FlowEngine = class {
|
|
|
4038
4170
|
* 양품률 시임(선택) — **이력에서 배운 수율.** 소요시간과 같은 자리·같은 규율이다 (2026-08-19).
|
|
4039
4171
|
*
|
|
4040
4172
|
* 없으면 선언값(현장→원천), 그것도 없으면 도메인 상수. 무엇을 썼는지는 `specCoverage()` 가 밝힌다.
|
|
4041
|
-
* 주입은 호스트가 한다(저널을
|
|
4173
|
+
* 주입은 호스트가 한다(저널을 계산하는 쪽이 이력을 안다) — 커널은 시임만 든다.
|
|
4042
4174
|
*/
|
|
4043
4175
|
yieldEstimator;
|
|
4044
4176
|
/**
|
|
@@ -4060,7 +4192,7 @@ var FlowEngine = class {
|
|
|
4060
4192
|
* 읽는 쪽(`paramNumber`·`paramDuration`)이 명세 행보다 이것을 먼저 본다.
|
|
4061
4193
|
*/
|
|
4062
4194
|
localParams = /* @__PURE__ */ new Map();
|
|
4063
|
-
/** 관측 구동(P0) — 이벤트를
|
|
4195
|
+
/** 관측 구동(P0) — 이벤트를 계산하는 투영기와 그 사실. tick 과 섞이지 않게 명시적으로 들고 있다. */
|
|
4064
4196
|
observer;
|
|
4065
4197
|
/**
|
|
4066
4198
|
* **관측 리듀서의 재개점을 꺼낸다** — 호스트가 저장해 다음 기동에서 되돌릴 수 있게.
|
|
@@ -4333,7 +4465,7 @@ var FlowEngine = class {
|
|
|
4333
4465
|
* 미러의 진실은 원천이다. 그래서 재고·위치·설비 상태는 스냅샷에서 심지 않는다 — 다음 계측·다음
|
|
4334
4466
|
* 마스터 재읽기가 곧 정정하므로, 심으면 **떠난 물건이 되살아나는** 위험만 남는다.
|
|
4335
4467
|
*
|
|
4336
|
-
* 그런데 원천이 **애초에 다시 말해 주지 않는 축**이 있다. 우리가
|
|
4468
|
+
* 그런데 원천이 **애초에 다시 말해 주지 않는 축**이 있다. 우리가 계산해 만든 것들이다:
|
|
4337
4469
|
* · 확인(ack)·조건이 처음 성립한 시각 — 사람이 한 일과 우리가 기억한 시각
|
|
4338
4470
|
* · 열린 구간의 누적·적산 기준점·관측 이후 최대 — SCADA 는 「이번 15분에 지금까지 얼마」를 모른다
|
|
4339
4471
|
*
|
|
@@ -4431,6 +4563,20 @@ var FlowEngine = class {
|
|
|
4431
4563
|
...m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {},
|
|
4432
4564
|
/* 결과도 이어받는다 — 잃으면 예측이 **자격 만료를 모르는 현장**에서 출발한다(낙관 쪽으로 치우친다). */
|
|
4433
4565
|
...m.testResults ? { testResults: m.testResults } : {},
|
|
4566
|
+
/*
|
|
4567
|
+
* ── 에너지 값도 이어받는다 (2026-08-26) ────────────────────────────────
|
|
4568
|
+
*
|
|
4569
|
+
* 이 자리가 에너지 필드를 심지 않았다. 그래서 재기동하면 설비의 발전량·충전율·계량값이 빈 채로
|
|
4570
|
+
* 시작하고, 다음 표본이 올 때까지 화면이 비어 보였다. 태양광 발전소에서 관측했다 — 저장된
|
|
4571
|
+
* 상태에는 `generatedKW` 가 있는데 되살린 트윈에는 없었다.
|
|
4572
|
+
*
|
|
4573
|
+
* 어느 필드를 이어받는지는 **능력 선언에서 뽑는다**(`ENERGY_STATE_FIELDS`). 스냅샷을 만드는
|
|
4574
|
+
* 쪽(`energyFieldsOf`)과 같은 목록이다 — 두 곳에 손으로 적으면 한쪽이 빠진 날 값이 조용히
|
|
4575
|
+
* 사라진다. 실제로 오늘 그렇게 났다.
|
|
4576
|
+
*
|
|
4577
|
+
* 값이 없으면 키를 만들지 않는다 — 에너지에서 「모른다」와 「0 이다」는 요금이 갈리는 차이다.
|
|
4578
|
+
*/
|
|
4579
|
+
...energyFieldsOf(m),
|
|
4434
4580
|
...effectiveOnly(m)
|
|
4435
4581
|
});
|
|
4436
4582
|
}
|
|
@@ -4679,7 +4825,7 @@ var FlowEngine = class {
|
|
|
4679
4825
|
*/
|
|
4680
4826
|
_acked = /* @__PURE__ */ new Set();
|
|
4681
4827
|
/**
|
|
4682
|
-
* 완료 시점에 **주체가 사라져
|
|
4828
|
+
* 완료 시점에 **주체가 사라져 제외한 작업 수** — 조용한 누락이 되지 않게 센다.
|
|
4683
4829
|
*
|
|
4684
4830
|
* 물품이 포장·출하·소비로 없어지는 것은 정상이지만, 그 물품을 향한 작업이 남아 있었다는 것은
|
|
4685
4831
|
* 상류에 어긋남이 있다는 신호다. 0 이 아니면 그 사실을 소비처가 볼 수 있어야 한다.
|
|
@@ -5012,9 +5158,13 @@ var FlowEngine = class {
|
|
|
5012
5158
|
if (cs.length) criteriaOf.set(n.id, cs);
|
|
5013
5159
|
}
|
|
5014
5160
|
}
|
|
5161
|
+
const silence = typeof this.observer?.equipmentSilence === "function" ? this.observer.equipmentSilence() : void 0;
|
|
5015
5162
|
const out = deriveAttentions(
|
|
5016
5163
|
{
|
|
5017
|
-
equipment: [...this.equipment.values()]
|
|
5164
|
+
equipment: [...this.equipment.values()].map((m) => {
|
|
5165
|
+
const s = silence?.get(m.id);
|
|
5166
|
+
return s ? { ...m, silentMs: s.silentMs, longestSilenceBeforeMs: s.longestBeforeMs } : m;
|
|
5167
|
+
}),
|
|
5018
5168
|
locations: [...this.locations.values()].map((n) => {
|
|
5019
5169
|
const criteria = criteriaOf.get(n.id);
|
|
5020
5170
|
return criteria?.length ? { ...n, criteria } : n;
|
|
@@ -5217,7 +5367,7 @@ var FlowEngine = class {
|
|
|
5217
5367
|
* 어긋난다(2026-08-01 하루에 아홉 곳). 근본 해법은 **한 상태 모델 두 구동**이고, 이것은 그 실현
|
|
5218
5368
|
* 가능성을 재는 스파이크다(design/plans/kernel-unification-live-observe.md P0).
|
|
5219
5369
|
*
|
|
5220
|
-
* 여기서는 **이미 검증된 조각을 조립**한다: 투영기가 이벤트를
|
|
5370
|
+
* 여기서는 **이미 검증된 조각을 조립**한다: 투영기가 이벤트를 계산하고, 그 결과를 씨앗 경로
|
|
5221
5371
|
* (`hydrateObserved`)로 커널 상태에 주입한다. 그래서 관측으로 실행한 커널을 그대로 `fork`·`tick` 할 수
|
|
5222
5372
|
* 있다 — "미러에서 예측한다" 가 별도 배관 없이 성립하는지가 이 스파이크의 질문이다.
|
|
5223
5373
|
*
|
|
@@ -6190,7 +6340,7 @@ var FlowEngine = class {
|
|
|
6190
6340
|
*
|
|
6191
6341
|
* 그때 도메인 훅은 없는 것을 딛으려다 오류를 낸다(`order.gtin` · `item.location`). 그 예외 하나가
|
|
6192
6342
|
* **예측 전체를 죽였다** — 사용자에게는 기능이 통째로 사라진 것으로 보였다. 그래서 완료 **전에**
|
|
6193
|
-
* 여기서 묻고, 없으면 그 작업만
|
|
6343
|
+
* 여기서 묻고, 없으면 그 작업만 제외한다.
|
|
6194
6344
|
*/
|
|
6195
6345
|
missingContextOf(t) {
|
|
6196
6346
|
if (t.itemEpc && !this.itemByRef(t.itemEpc)) return "item";
|
|
@@ -7639,7 +7789,7 @@ var MesKernel = class extends FlowEngine {
|
|
|
7639
7789
|
*
|
|
7640
7790
|
* 씨앗이 이미 이행된 오더를 주입하지 않으므로(남은 수량 0) 그 오더에 딸린 작업만 남을 수 있고,
|
|
7641
7791
|
* 관측이 오더 연결을 담지 못한 작업도 있다. 그때 예전에는 `order.gtin` 에서 오류가 났고 **그 예외
|
|
7642
|
-
* 하나가 정확도 추세 전체를 죽였다.** 이제 코어가 이 답을 보고 그 작업만
|
|
7792
|
+
* 하나가 정확도 추세 전체를 죽였다.** 이제 코어가 이 답을 보고 그 작업만 제외한다.
|
|
7643
7793
|
*/
|
|
7644
7794
|
canComplete(t) {
|
|
7645
7795
|
return !!(t.orderId && this.orders.get(t.orderId));
|
|
@@ -8156,7 +8306,7 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8156
8306
|
* **뿌리 계량기** — 조상 중에 계량된 자리가 없는 계량 지점들.
|
|
8157
8307
|
*
|
|
8158
8308
|
* 계층 계량(수전 ⊃ 분기)에서 전부 더하면 이중 계상이 된다. 뿌리만 더하면 현장의 실제 부하가 된다.
|
|
8159
|
-
* 모델이 그 계량기의 자리를 모르면 뿌리로 본다(모르는 것을 빼면 부하가
|
|
8309
|
+
* 모델이 그 계량기의 자리를 모르면 뿌리로 본다(모르는 것을 빼면 부하가 오류 없이 작아진다 — 그것이
|
|
8160
8310
|
* 「계약 안쪽」이라는 더 위험한 거짓을 만든다).
|
|
8161
8311
|
*/
|
|
8162
8312
|
rootMeterIds() {
|
|
@@ -8229,7 +8379,7 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8229
8379
|
* "지금" 이 시뮬 기준시각(2026-01-01)에 얼어, 구간 마감·피크·감축 제안이 전부 8개월 전으로 기록됐다.
|
|
8230
8380
|
*
|
|
8231
8381
|
* 계측 자체는 원천 시각을 달고 있어서 화면의 순간값은 정상으로 보였다 — 그래서 조용한 결함이었다.
|
|
8232
|
-
* 저널을 기간으로
|
|
8382
|
+
* 저널을 기간으로 계산하는 쪽(성과·요금·피더 배분)은 그 트윈에서 영원히 아무것도 찾지 못했다.
|
|
8233
8383
|
*/
|
|
8234
8384
|
apply(envelope) {
|
|
8235
8385
|
if (envelope.eventType === ENERGY_EVENT.measured) {
|
|
@@ -8240,6 +8390,10 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8240
8390
|
this.applyEquipmentEnergy(envelope);
|
|
8241
8391
|
return;
|
|
8242
8392
|
}
|
|
8393
|
+
if (envelope.eventType === ENERGY_EVENT.generated) {
|
|
8394
|
+
this.applyGenerated(envelope);
|
|
8395
|
+
return;
|
|
8396
|
+
}
|
|
8243
8397
|
super.apply(envelope);
|
|
8244
8398
|
}
|
|
8245
8399
|
/**
|
|
@@ -8254,6 +8408,51 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8254
8408
|
*
|
|
8255
8409
|
* 온 값만 덮는다: 보내지 않은 필드는 그대로 둔다(없는 값을 0 으로 만들지 않는다).
|
|
8256
8410
|
*/
|
|
8411
|
+
/**
|
|
8412
|
+
* **지금까지 만든 양**(적산)을 설비에 세운다 — 이 사건 이름은 오래 선언만 되어 있었다.
|
|
8413
|
+
*
|
|
8414
|
+
* ── 무엇이 없었나 (2026-08-26) ─────────────────────────────────────────────
|
|
8415
|
+
* 설계 문서와 계약의 표에 `energy.generated` 가 있고 **내는 곳이 한 곳도 없었다.** 그래서 태양광
|
|
8416
|
+
* 발전소를 붙인 현장에서 「지금 발전 중」은 보이고 「얼마나 발전했나」는 보이지 않았다(호현에너지
|
|
8417
|
+
* 실측: 하루에 174kWh 가 자랐는데 트윈에는 없었다).
|
|
8418
|
+
*
|
|
8419
|
+
* ── 되돌아간 적산을 메우지 않는다 ──────────────────────────────────────────
|
|
8420
|
+
* 인버터를 교체하면 적산이 0부터 다시 오른다. 그 계단을 우리가 메우면 **없던 발전량이 생긴다.** 그래서
|
|
8421
|
+
* 값은 연결된 시스템이 준 대로 두고 「이 시각에 되돌아갔다」만 표시한다. 차분을 구하는 쪽이 그 시점을 건너
|
|
8422
|
+
* 계산할 수 있어야 하고, 그 판단은 그쪽의 것이다.
|
|
8423
|
+
*
|
|
8424
|
+
* ── 늦게 온 옛 표본은 최신을 덮지 않는다 ───────────────────────────────────
|
|
8425
|
+
* 계량 지점과 같은 규율이다(`point.atMs` 비교). 사건이 일어난 순서대로 도착하지 않으므로, 시각을 비교하지
|
|
8426
|
+
* 않으면 늦게 온 옛 적산이 최신 값을 뒤로 돌린다 — 그리고 그것이 「계기가 되돌아갔다」로 잘못 적힌다.
|
|
8427
|
+
*/
|
|
8428
|
+
applyGenerated(envelope) {
|
|
8429
|
+
const d = envelope.data;
|
|
8430
|
+
const id = String(d?.equipmentId ?? "").trim();
|
|
8431
|
+
const eq = id ? this.equipment.get(id) : void 0;
|
|
8432
|
+
if (!eq) {
|
|
8433
|
+
this.unknownEquipmentReports++;
|
|
8434
|
+
return;
|
|
8435
|
+
}
|
|
8436
|
+
const kWh = Number(d?.kWh);
|
|
8437
|
+
if (!Number.isFinite(kWh)) return;
|
|
8438
|
+
const atMs = [String(d?.at ?? ""), String(envelope?.eventTime ?? "")].map((v) => Date.parse(v)).find((v) => Number.isFinite(v));
|
|
8439
|
+
if (atMs === void 0) return;
|
|
8440
|
+
this.noteObserved(atMs);
|
|
8441
|
+
const heardAt = this.generationHeardAtMs.get(id);
|
|
8442
|
+
if (heardAt !== void 0 && atMs < heardAt) return;
|
|
8443
|
+
this.generationHeardAtMs.set(id, atMs);
|
|
8444
|
+
const before = Number(eq.generatedKWh);
|
|
8445
|
+
if (Number.isFinite(before) && kWh < before) {
|
|
8446
|
+
;
|
|
8447
|
+
eq.generatedKWhResetAt = new Date(atMs).toISOString();
|
|
8448
|
+
}
|
|
8449
|
+
;
|
|
8450
|
+
eq.generatedKWh = kWh;
|
|
8451
|
+
eq.measuredAt = new Date(atMs).toISOString();
|
|
8452
|
+
this.revision++;
|
|
8453
|
+
}
|
|
8454
|
+
/** 설비마다 발전 적산을 마지막으로 들은 시각 — 늦게 온 옛 표본을 가리는 데만 쓴다. */
|
|
8455
|
+
generationHeardAtMs = /* @__PURE__ */ new Map();
|
|
8257
8456
|
applyEquipmentEnergy(envelope) {
|
|
8258
8457
|
const d = envelope.data;
|
|
8259
8458
|
const id = String(d?.equipmentId ?? "").trim();
|
|
@@ -8293,6 +8492,9 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8293
8492
|
const kW = Number.isFinite(Number(d?.kW)) ? Number(d.kW) : void 0;
|
|
8294
8493
|
const point = this.points.get(id) ?? { id, samplesInWindow: 0 };
|
|
8295
8494
|
point.origin = this.equipment.has(id) ? "master" : "observed";
|
|
8495
|
+
const declaredDirection = this.meterDirectionOf(id);
|
|
8496
|
+
if (declaredDirection) point.direction = declaredDirection;
|
|
8497
|
+
else delete point.direction;
|
|
8296
8498
|
if (point.atMs === void 0 || atMs >= point.atMs) {
|
|
8297
8499
|
point.atMs = atMs;
|
|
8298
8500
|
if (kW !== void 0) point.kW = kW;
|
|
@@ -8307,12 +8509,18 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8307
8509
|
const roots = this.rootMeterIds();
|
|
8308
8510
|
let total = 0;
|
|
8309
8511
|
let declared = 0;
|
|
8512
|
+
let exported = 0;
|
|
8310
8513
|
for (const p of this.points.values()) {
|
|
8311
8514
|
if (!roots.has(p.id)) continue;
|
|
8312
8515
|
const v = p.kW ?? 0;
|
|
8516
|
+
if (p.direction === "export") {
|
|
8517
|
+
exported += v;
|
|
8518
|
+
continue;
|
|
8519
|
+
}
|
|
8313
8520
|
total += v;
|
|
8314
8521
|
if (this.equipment.has(p.id)) declared += v;
|
|
8315
8522
|
}
|
|
8523
|
+
if (exported > 0 && (w.exportKW === void 0 || exported > w.exportKW)) w.exportKW = exported;
|
|
8316
8524
|
if (w.maxKW === void 0 || total > w.maxKW) {
|
|
8317
8525
|
w.maxKW = total;
|
|
8318
8526
|
w.maxDeclaredKW = declared;
|
|
@@ -8379,6 +8587,7 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8379
8587
|
/* 총부하도 사실로 남긴다 — 이것이 없으면 저널을 읽는 쪽은 「무엇이 깎였나」를 되짚을 수 없다
|
|
8380
8588
|
(요금은 순수요로 매겨지지만, 그 수요를 만든 것은 설비 부하다). */
|
|
8381
8589
|
...w.grossMaxKW !== void 0 ? { grossMaxKW: w.grossMaxKW } : {},
|
|
8590
|
+
...w.exportKW !== void 0 ? { exportKW: w.exportKW } : {},
|
|
8382
8591
|
...w.meanKW !== void 0 ? { meanKW: w.meanKW } : {},
|
|
8383
8592
|
samples: w.samples,
|
|
8384
8593
|
...w.contractKW !== void 0 ? { contractKW: w.contractKW } : {},
|
|
@@ -8395,7 +8604,7 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8395
8604
|
* ── 지점별 몫을 함께 낸다 (2026-08-18) ────────────────────────────────
|
|
8396
8605
|
*
|
|
8397
8606
|
* 분기마다 계량기가 있는 현장에서 분기별 전력량은 **측정**이다. 그런데 그 사실이 저널에
|
|
8398
|
-
* 없어서, 저널을
|
|
8607
|
+
* 없어서, 저널을 계산하는 쪽은 가동시간으로 **배분**할 수밖에 없었다 — 계량이 있는데 배분으로
|
|
8399
8608
|
* 말하는 것은 근거를 실제보다 약하게 보고하는 것이고, 그 위에 세운 SEU 판단이 무거운 값을 놓친다.
|
|
8400
8609
|
*
|
|
8401
8610
|
* 표본을 그대로 저널에 흘리는 대신 **구간마다 한 줄**로 낸다: 계측은 초당 여러 건이라 표본을
|
|
@@ -8731,6 +8940,23 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8731
8940
|
return void 0;
|
|
8732
8941
|
}
|
|
8733
8942
|
/** 글자로 선언된 값(발전 형상·통화 같은 것) — 비어 있으면 없는 것으로 본다. */
|
|
8943
|
+
/**
|
|
8944
|
+
* 이 계량 지점의 선언된 방향 — 자리와 설비 둘 다 본다(수전 자리에도, 계량기 설비에도 걸린다).
|
|
8945
|
+
*
|
|
8946
|
+
* 선언에 없는 낱말은 **받지 않는다**. 「outgoing」·「생산」처럼 뜻이 통할 것 같은 말을 받아 주면
|
|
8947
|
+
* 현장마다 다른 어휘가 쌓이고, 그중 하나를 우리가 잘못 읽는 날 발전이 부하로 세어진다.
|
|
8948
|
+
*/
|
|
8949
|
+
meterDirectionOf(id) {
|
|
8950
|
+
const declared = this.stringProperty(
|
|
8951
|
+
(this.boardDef?.equipment ?? []).find((e) => e.id === id),
|
|
8952
|
+
EMS_PROPERTY.meterDirection
|
|
8953
|
+
) ?? this.stringProperty(
|
|
8954
|
+
(this.boardDef?.locations ?? []).find((l) => l.id === id),
|
|
8955
|
+
EMS_PROPERTY.meterDirection
|
|
8956
|
+
);
|
|
8957
|
+
if (!declared) return void 0;
|
|
8958
|
+
return METER_DIRECTION.includes(declared) ? declared : void 0;
|
|
8959
|
+
}
|
|
8734
8960
|
stringProperty(resource, id) {
|
|
8735
8961
|
for (const p of resource?.properties ?? []) {
|
|
8736
8962
|
if (p?.id !== id) continue;
|
|
@@ -8749,10 +8975,10 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8749
8975
|
* 에너지의 연속성 — **SCADA 는 「이번 15분에 지금까지 얼마」를 모른다.**
|
|
8750
8976
|
*
|
|
8751
8977
|
* 계량기는 순간값과 적산값을 준다. 「이 구간의 최대」·「구간이 열릴 때의 적산」·「관측 이후 최대」는
|
|
8752
|
-
* 우리가
|
|
8978
|
+
* 우리가 계산해 만든 값이라, 재기동하면 원천이 되풀어 주지 않는다. 잃으면 오류 없이 값이 작아진다 —
|
|
8753
8979
|
* 구간 전력량이 기준점을 새로 잡아 짧아지고, 그 구간의 피크가 부팅 이후로만 잡힌다(요금이 걸린 수다).
|
|
8754
8980
|
*
|
|
8755
|
-
* 지난 마감 구간도 이어받는다: 이미 저널에 있는 사실이지만, 화면이 그것을 보려고 매번 저널을
|
|
8981
|
+
* 지난 마감 구간도 이어받는다: 이미 저널에 있는 사실이지만, 화면이 그것을 보려고 매번 저널을 계산하지
|
|
8756
8982
|
* 않게 한다(자른 사실은 `closedTotal` 이 그대로 나른다).
|
|
8757
8983
|
*
|
|
8758
8984
|
* 지금 열린 구간이 **이미 지나간 것**이어도 그대로 받는다 — 다음 표본이 오면 커널이 그 구간을 정상
|
|
@@ -8930,6 +9156,44 @@ function ingestEnergyEquipmentRecords(records, opts) {
|
|
|
8930
9156
|
}
|
|
8931
9157
|
return { accepted, rejected };
|
|
8932
9158
|
}
|
|
9159
|
+
function isEnergyGenerationRecord(record) {
|
|
9160
|
+
if (!record || typeof record !== "object") return false;
|
|
9161
|
+
const r = record;
|
|
9162
|
+
return typeof r.equipmentId === "string" && r.equipmentId.trim().length > 0 && r.kWh !== void 0 && r.meterId === void 0 && r.epc === void 0;
|
|
9163
|
+
}
|
|
9164
|
+
function ingestEnergyGenerationRecords(records, opts) {
|
|
9165
|
+
const list = records === void 0 || records === null ? [] : Array.isArray(records) ? records : [records];
|
|
9166
|
+
const accepted = [];
|
|
9167
|
+
const rejected = [];
|
|
9168
|
+
let seq = 0;
|
|
9169
|
+
for (const r of list) {
|
|
9170
|
+
const errors = [];
|
|
9171
|
+
const equipmentId = String(r?.equipmentId ?? "").trim();
|
|
9172
|
+
if (!equipmentId) errors.push("equipmentId \uAC00 \uC5C6\uB2E4 \u2014 \uC5B4\uB290 \uC124\uBE44\uAC00 \uB9CC\uB4E0 \uAC83\uC778\uC9C0 \uC9C0\uC5B4\uB0BC \uC218 \uC5C6\uB2E4");
|
|
9173
|
+
const raw = r?.kWh;
|
|
9174
|
+
const kWh = Number(raw);
|
|
9175
|
+
if (raw === void 0 || raw === null || !Number.isFinite(kWh)) {
|
|
9176
|
+
errors.push("kWh \uAC00 \uC5C6\uB2E4 \u2014 \uC774 \uBB38\uC774 \uBC1B\uB294 \uAC12\uC740 \uC801\uC0B0 \uD558\uB098\uB2E4(\uC21C\uC2DC \uCD9C\uB825\uC740 \uC124\uBE44 \uC0C1\uD0DC\uB85C \uBCF4\uB0B8\uB2E4)");
|
|
9177
|
+
} else if (kWh < 0) {
|
|
9178
|
+
errors.push(`kWh \uAC00 \uC74C\uC218\uB2E4(${kWh}) \u2014 \uC801\uC0B0\uC740 \uB4A4\uB85C \uAC00\uB354\uB77C\uB3C4 \uC74C\uC218\uAC00 \uB418\uC9C0 \uC54A\uB294\uB2E4`);
|
|
9179
|
+
}
|
|
9180
|
+
const eventTime = String(r?.at ?? "").trim() || opts.defaultEventTime || "";
|
|
9181
|
+
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");
|
|
9182
|
+
if (errors.length) {
|
|
9183
|
+
rejected.push({ record: r, errors });
|
|
9184
|
+
continue;
|
|
9185
|
+
}
|
|
9186
|
+
const data = { equipmentId, kWh, at: eventTime };
|
|
9187
|
+
accepted.push({
|
|
9188
|
+
eventId: `${opts.tenantId}-generated-${++seq}`,
|
|
9189
|
+
eventType: ENERGY_EVENT.generated,
|
|
9190
|
+
eventTime,
|
|
9191
|
+
tenantId: opts.tenantId,
|
|
9192
|
+
data
|
|
9193
|
+
});
|
|
9194
|
+
}
|
|
9195
|
+
return { accepted, rejected };
|
|
9196
|
+
}
|
|
8933
9197
|
|
|
8934
9198
|
// src/operational-ingest.ts
|
|
8935
9199
|
var TASK_STATUS = ["created", "assigned", "in-progress", "completed"];
|
|
@@ -8940,7 +9204,7 @@ var SPECS = {
|
|
|
8940
9204
|
task: {
|
|
8941
9205
|
eventType: OP_EVENT.task,
|
|
8942
9206
|
identity: "taskId",
|
|
8943
|
-
/* 종류가 없으면 성과를 종류별로
|
|
9207
|
+
/* 종류가 없으면 성과를 종류별로 모을 수 없고(선언된 시간·수율이 종류로 붙는다) 지어낼 수도 없다. */
|
|
8944
9208
|
required: ["taskId", "kind", "status"],
|
|
8945
9209
|
fields: {
|
|
8946
9210
|
taskId: "string",
|
|
@@ -9193,7 +9457,7 @@ function ingestOperationalRecords(records, opts) {
|
|
|
9193
9457
|
}
|
|
9194
9458
|
const allowed = spec.enums?.[name];
|
|
9195
9459
|
if (allowed && !allowed.includes(v)) {
|
|
9196
|
-
errors.push(`${kind}.${name} \uC774 \uCEE4\uB110\uC774 \
|
|
9460
|
+
errors.push(`${kind}.${name} \uC774 \uCEE4\uB110\uC774 \uB2E4\uB8E8\uB294 \uB0B1\uB9D0\uC774 \uC544\uB2C8\uB2E4: ${JSON.stringify(v)} \u2014 \uBC1B\uB294 \uAC12\uC740 ${allowed.join(" \xB7 ")}`);
|
|
9197
9461
|
break;
|
|
9198
9462
|
}
|
|
9199
9463
|
data[name] = v;
|
|
@@ -9532,6 +9796,7 @@ function retiredVocabularyIn(line) {
|
|
|
9532
9796
|
MES_BIZSTEP,
|
|
9533
9797
|
MES_LOCATION_TYPES,
|
|
9534
9798
|
MES_TYPES,
|
|
9799
|
+
METER_DIRECTION,
|
|
9535
9800
|
MesKernel,
|
|
9536
9801
|
OPERATION_PROPERTY,
|
|
9537
9802
|
OP_EVENT,
|
|
@@ -9600,11 +9865,13 @@ function retiredVocabularyIn(line) {
|
|
|
9600
9865
|
inWorkCalendarAt,
|
|
9601
9866
|
ingest,
|
|
9602
9867
|
ingestEnergyEquipmentRecords,
|
|
9868
|
+
ingestEnergyGenerationRecords,
|
|
9603
9869
|
ingestEnergyRecords,
|
|
9604
9870
|
ingestOperationalRecords,
|
|
9605
9871
|
isAggregationRecord,
|
|
9606
9872
|
isElectricalLocationType,
|
|
9607
9873
|
isEnergyEquipmentRecord,
|
|
9874
|
+
isEnergyGenerationRecord,
|
|
9608
9875
|
isEnergyRecord,
|
|
9609
9876
|
isEquipmentLevel,
|
|
9610
9877
|
isOperationalRecord,
|