@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.
@@ -51,6 +51,8 @@ export interface CurtailableState {
51
51
  export interface GeneratingState {
52
52
  generatedKW?: number;
53
53
  exportKW?: number;
54
+ generatedKWh?: number;
55
+ generatedKWhResetAt?: string;
54
56
  }
55
57
  /** 저장 — 담고 낸다. 충전과 방전을 나눈다(손실·수명 판단이 둘을 구별해야 한다). */
56
58
  export interface StoringState {
@@ -78,7 +78,15 @@ export const CAPABILITIES = {
78
78
  energyGenerating: {
79
79
  key: 'energyGenerating', label: 'twin.capability.energyGenerating', semantics: '전기를 만든다 — 역송은 소비의 음수가 아니라 별개 사실이다.',
80
80
  commands: [],
81
- stateFields: ['generatedKW', 'exportKW'], models: ['GeneratingState'], results: ['generated']
81
+ /*
82
+ * ── 적산을 더한다 (2026-08-26) ─────────────────────────────────────────────
83
+ * 이 능력이 순시 출력만 선언하고 있었다. 그래서 「지금 발전 중」은 말하고 「얼마나 발전했나」는 말할
84
+ * 수 없었다 — 사건 이름(`energy.generated`)과 설계 문서에는 처음부터 있던 사실이다.
85
+ *
86
+ * 계량 능력(`metered`)이 `kW` 와 `kWh` 를 함께 선언한 것과 같은 짝이다. 적산은 발전 설비의 보편적
87
+ * 사실이고(태양광·열병합·디젤), 그것이 없으면 성능비·발전시간 같은 성과를 아무도 셀 수 없다.
88
+ */
89
+ stateFields: ['generatedKW', 'exportKW', 'generatedKWh', 'generatedKWhResetAt'], models: ['GeneratingState'], results: ['generated']
82
90
  },
83
91
  /*
84
92
  * ── 왜 `storing` 이 아니라 `energyStoring` 인가 (2026-08-15) ────────────────
@@ -43,7 +43,7 @@ export interface CanonicalEnvelope<T = unknown> {
43
43
  * 로 부르면 **표준 이름을 쓰면서 다른 뜻으로 쓰는 것**이 된다. 대조 문서가 그 판정을 적어 두었다.
44
44
  *
45
45
  * ── 왜 **닫혀** 있나 ────────────────────────────────────────────────────────
46
- * 커널이 이 넷으로 접는다: 성과 폴드가 착수·완료로 갈리고(`foldTaskRecords`), 주목 계산이
46
+ * 커널이 이 넷으로 처리한다: 성과 폴드가 착수·완료로 갈리고(`foldTaskRecords`), 주목 계산이
47
47
  * `created`·`assigned` 를 대기로 센다, 배정 루프는 `created` 만 집는다. 그러니 낱말이 하나 늘면
48
48
  * 그 셋이 조용히 달라진다 — 유입 문이 이 넷만 받는 이유다(다른 낱말은 오류 없이 사라진다).
49
49
  */
@@ -384,7 +384,7 @@ export interface TestResult {
384
384
  /**
385
385
  * `OP_EVENT.test` 의 페이로드 — **결과가 대상을 가리킨다.**
386
386
  *
387
- * 표준(`TestResultType`)의 방향 그대로다. 상태에서는 개체 안에 접어 두지만(소비처가 대상별로 묻는다)
387
+ * 표준(`TestResultType`)의 방향 그대로다. 상태에서는 개체 안에 계산해 두지만(소비처가 대상별로 묻는다)
388
388
  * 사건에서는 가리킨다 — 그래야 대상이 무엇이든(로트·설비·사람·자리) 한 채널로 들어온다.
389
389
  */
390
390
  export interface TestResultFact extends TestResult {
@@ -557,7 +557,7 @@ export interface PropertyMeasurement extends StandardValue {
557
557
  /** 표준 `MeasurementDate` — 언제 쟀나. */
558
558
  measurementDate?: ISOTime;
559
559
  /**
560
- * 이 값이 **잰 것이 아니라 접은 것**임을 밝힌다 — §`DemandWindowState.derived` 와 같은 규율.
560
+ * 이 값이 **잰 것이 아니라 계산한 값**임을 밝힌다 — §`DemandWindowState.derived` 와 같은 규율.
561
561
  *
562
562
  * 트윈은 원본이 주지 않는 값을 계산할 수 있다(그것이 트윈의 존재 이유다). 그런데 그 수를 실측과
563
563
  * **같은 자리에 같은 모양으로** 두면 화면·보고서가 그것을 잰 값으로 읽는다. 규제 기록에서 그
@@ -758,7 +758,7 @@ export interface MaterialQuantity {
758
758
  /**
759
759
  * 물품을 구별하는 키 — 직렬 물품은 `epc`, 로트의 부분은 `subLotId`(표준 `MaterialSubLot.ID`).
760
760
  *
761
- * **한 곳에서 정한다.** 소비처마다 `epc` 로 키를 잡으면 같은 로트의 두 부분이 하나로 접히고,
761
+ * **한 곳에서 정한다.** 소비처마다 `epc` 로 키를 잡으면 같은 로트의 두 부분이 하나로 합쳐지고,
762
762
  * 그 순간 재고가 조용히 줄어든다(실제로 그랬다 — §ItemState.subLotId).
763
763
  *
764
764
  * ── 규약 (2026-08-21에 확정) ────────────────────────────────────────────────
@@ -1179,7 +1179,7 @@ export interface LocationObservation extends StandardValue {
1179
1179
  /** 누가 말했나 — 표준 `Source`. 계측기인지 사람이 적은 장부인지는 이 값이 답한다. */
1180
1180
  source?: string;
1181
1181
  /**
1182
- * 이 값이 **잰 것이 아니라 접은 것**임을 밝힌다 — §`PropertyMeasurement.derived` 와 같은 규율.
1182
+ * 이 값이 **잰 것이 아니라 계산한 값**임을 밝힌다 — §`PropertyMeasurement.derived` 와 같은 규율.
1183
1183
  *
1184
1184
  * 트윈은 원본이 주지 않는 값을 계산할 수 있다(그것이 존재 이유다). 그러나 실측과 같은 자리에 같은
1185
1185
  * 모양으로 두면 보고서가 그것을 잰 값으로 읽는다. 규제 기록에서 그 구별이 사라지는 것은 결함이
@@ -1265,7 +1265,7 @@ export interface ItemState {
1265
1265
  * 기록이고 `TestableObjectID` 로 **대상을 가리킨다**(B2MML-OperationsTest.xsd).
1266
1266
  *
1267
1267
  * **그것이 우리가 좁힌 자리다**: 우리 상태는 대상별 투영이라(소비처가 「이 물품은?」을 묻는다) 가리키는
1268
- * 기록을 개체 안에 접어 둔다. 같은 사실이고 방향만 다르다 — 사건에서는 표준 그대로 대상을 가리킨다
1268
+ * 기록을 개체 안에 계산해 둔다. 같은 사실이고 방향만 다르다 — 사건에서는 표준 그대로 대상을 가리킨다
1269
1269
  * (§`OP_EVENT.test`). 자원에서 이미 같은 좁힘을 했다(`EquipmentState.testResults`).
1270
1270
  *
1271
1271
  * ── 명세당 하나인 이유 ────────────────────────────────────────────────────
@@ -1361,6 +1361,23 @@ export interface EquipmentState extends EffectivePeriod {
1361
1361
  /** generating — 만든 전력·역송(kW). */
1362
1362
  generatedKW?: number;
1363
1363
  exportKW?: number;
1364
+ /**
1365
+ * generating — **지금까지 만든 양**(kWh, 계기 적산값).
1366
+ *
1367
+ * 연결된 시스템이 준 값 그대로다(우리가 kW 를 적분한 값이 아니다). 「오늘 얼마나 발전했나」는 두 시점의 차이로
1368
+ * 얻는다 — 그 구간을 무엇으로 잡을지는 현장이 정하는 것이므로 커널이 구간을 정해 두지 않는다.
1369
+ */
1370
+ generatedKWh?: number;
1371
+ /**
1372
+ * 적산이 **되돌아간 시각** — 계기가 뒤로 간 것을 본 마지막 시점.
1373
+ *
1374
+ * 인버터를 교체하면 적산이 0부터 다시 오른다. 그것을 모르면 차분을 구하는 쪽이 그 계단을 「음의
1375
+ * 발전」으로 읽거나, 반대로 그 앞의 값을 잃는다. 되돌아간 사실을 **우리가 메우지 않고 표시만** 한다 —
1376
+ * 없던 발전량을 지어내는 것보다 「이 시점을 건너 계산하지 말라」고 말하는 것이 정직하다.
1377
+ *
1378
+ * 되돌아간 적이 없으면 이 칸이 없다.
1379
+ */
1380
+ generatedKWhResetAt?: ISOTime;
1364
1381
  /** storing — 충전율(%)·충전·방전(kW). 충전과 방전을 나눈다(손실·수명 판단이 그 둘을 구별한다). */
1365
1382
  soc?: number;
1366
1383
  chargeKW?: number;
@@ -1776,6 +1793,16 @@ export interface MeterPointState {
1776
1793
  * 커널은 표본이 올 때마다 이 값을 다시 맞춘다.
1777
1794
  */
1778
1795
  origin?: 'master' | 'observed';
1796
+ /**
1797
+ * **무엇을 재는 계량기인가** — 쓰는 쪽(`import`) · 내는 쪽(`export`) · 양쪽(`bidirectional`).
1798
+ *
1799
+ * 관측이 아니라 **선언**에서 온다(자원 속성 `meter.direction`). 표본마다 바뀌는 값이 아니고, 그
1800
+ * 계량기가 무엇을 재도록 설치되었는지는 현장이 안다. 그래서 표본에는 이 칸이 없다.
1801
+ *
1802
+ * 선언하지 않으면 이 칸이 없고, 그때 그 지점은 **부하로 센다**. 빼면 부하가 오류 없이 작아지고, 작아진
1803
+ * 부하는 「계약 안쪽」이라는 더 위험한 거짓을 만든다.
1804
+ */
1805
+ direction?: 'import' | 'export' | 'bidirectional';
1779
1806
  kW?: number;
1780
1807
  /** 누적 전력량(원천이 준 적산값) — 우리가 적분한 값이 아니다. */
1781
1808
  kWh?: number;
@@ -1799,7 +1826,7 @@ export interface DemandWindowState {
1799
1826
  /**
1800
1827
  * `maxKW` 가 찍힌 **그 순간**의, **모델이 선언한** 뿌리 계량기들만의 합.
1801
1828
  *
1802
- * `maxKW` 는 모르는 계량기까지 포함한 합이다(그것을 빼면 부하가 조용히 작아지고, 작아진 부하는
1829
+ * `maxKW` 는 모르는 계량기까지 포함한 합이다(그것을 빼면 부하가 오류 없이 작아지고, 작아진 부하는
1803
1830
  * 「계약 안쪽」이라는 더 위험한 거짓을 만든다). 그러나 그 값 하나만 내면 반대쪽 거짓이 생긴다 —
1804
1831
  * 오타로 생긴 유령 계량기가 계약 초과를 만들어도 화면은 그것을 진짜 초과로 말한다.
1805
1832
  *
@@ -1840,6 +1867,24 @@ export interface DemandWindowState {
1840
1867
  * 발전·축전을 선언하지 않은 현장에서는 `maxKW` 와 같다(그때는 차감할 것이 없다).
1841
1868
  */
1842
1869
  grossMaxKW?: number;
1870
+ /**
1871
+ * 이 구간에 **내보낸 최대**(kW) — 내는 쪽으로 선언된 계량 지점들의 합.
1872
+ *
1873
+ * 수요의 최대와 **같은 순간이 아니다.** 해가 가장 좋은 때와 부하가 가장 큰 때는 다르고, 한쪽 순간에
1874
+ * 맞춰 다른 쪽을 적으면 두 수 다 사실이 아니게 된다.
1875
+ *
1876
+ * ── 왜 빼지 않고 따로 내나 (2026-08-25) ───────────────────────────────────
1877
+ * 내보낸 양을 재는 계량은 수요에 **더하지 않는다**(그것이 이 축을 만든 이유다). 그렇다고 수요에서 **빼지도
1878
+ * 않는다**: 수전 계량기가 이미 상계 계량이면 그 값에 발전이 반영되어 있고, 거기서 또 빼면 두 번
1879
+ * 깎는다. 어느 쪽인지는 현장의 결선이 정하는 것이고 우리가 표본만 보고 알 수는 없다.
1880
+ *
1881
+ * 그래서 두 수를 나란히 낸다. 화면은 「계통에서 받은 최대 320kW, 같은 순간 내보낸 것 180kW」라고
1882
+ * 말할 수 있다. 자체 구동 경로는 설비의 발전량으로 순수요를 계산한다(§`grossMaxKW`) — 그것은 우리가
1883
+ * 만든 값이라 이중 계상이 없다.
1884
+ *
1885
+ * 잰 적이 없으면 `undefined` 다(0 이 아니다).
1886
+ */
1887
+ exportKW?: number;
1843
1888
  /** 표본 평균 부하 — 요금 산정의 평균 수요에 대응한다. */
1844
1889
  meanKW?: number;
1845
1890
  /** **kW 를 실은** 표본 수 — 부하 판정의 근거 수다(받은 표본 전체가 아니다). */
@@ -2333,6 +2378,35 @@ export interface EnergyEquipmentData {
2333
2378
  minKW?: number;
2334
2379
  position?: 'open' | 'closed' | 'intermediate' | 'bad';
2335
2380
  }
2381
+ /**
2382
+ * 발전 적산으로 담기는 값 — **발전 설비 하나가 지금까지 만든 양.**
2383
+ *
2384
+ * ── 왜 계량과 따로 있나 (2026-08-26) ───────────────────────────────────────
2385
+ * 이 사건 이름(`energy.generated`)은 오래 **선언만 되어 있었다.** 설계 문서(`profiles/ems.md` §4)와
2386
+ * 이 표에 이름이 있고, 내는 곳이 한 곳도 없었다. 그래서 태양광 발전소를 붙인 현장에서 「지금 발전
2387
+ * 중」은 보이고 「얼마나 발전했나」는 보이지 않았다(호현에너지 실측: 하루에 174kWh 가 자랐는데 트윈에는
2388
+ * 없었다).
2389
+ *
2390
+ * 계량(`EnergyMeasuredData`)으로 보낼 수는 없다. 계량 지점의 값은 부하 합에 들어가고(§`MeterPointState`),
2391
+ * 발전을 그 합에 넣으면 계약 대비 판단이 반대로 틀린다. 발전은 **음의 소비가 아니라 별개 사실**이다 —
2392
+ * 이 표의 `generated` 주석이 처음부터 그렇게 적어 두었다.
2393
+ *
2394
+ * ── 왜 순시 출력(kW)은 여기 없나 ───────────────────────────────────────────
2395
+ * 그것은 이미 갈 곳이 있다(`EnergyEquipmentData.generatedKW`). 같은 값을 두 문으로 받으면 어느 쪽이
2396
+ * 맞는지 정하는 규칙이 또 필요해진다. 이 문은 **적산만** 받는다 — 그것이 없던 것이다.
2397
+ */
2398
+ export interface EnergyGeneratedData {
2399
+ /** 발전 설비(id) — 계량 지점이 아니다. */
2400
+ equipmentId: string;
2401
+ /**
2402
+ * 계기 적산값(kWh) — **연결된 시스템이 준 값 그대로.** 차분은 소비처가 한다.
2403
+ *
2404
+ * 우리가 kW 를 적분해 만들지 않는다. 적산은 그 계기 자신의 회계이고, 우리가 적분한 값과 미세하게
2405
+ * 다르며 다를 때 맞는 쪽은 계기다(계량 쪽과 같은 규율).
2406
+ */
2407
+ kWh: number;
2408
+ at: ISOTime;
2409
+ }
2336
2410
  /** 계량 도착의 실린 값 — 계량 지점 하나의 한 시점. */
2337
2411
  export interface EnergyMeasuredData {
2338
2412
  /** 계량 지점(설비 id). */
package/dist/contract.js CHANGED
@@ -519,7 +519,7 @@ export function dueStatusOf(x, nowIso) {
519
519
  /**
520
520
  * 물품을 구별하는 키 — 직렬 물품은 `epc`, 로트의 부분은 `subLotId`(표준 `MaterialSubLot.ID`).
521
521
  *
522
- * **한 곳에서 정한다.** 소비처마다 `epc` 로 키를 잡으면 같은 로트의 두 부분이 하나로 접히고,
522
+ * **한 곳에서 정한다.** 소비처마다 `epc` 로 키를 잡으면 같은 로트의 두 부분이 하나로 합쳐지고,
523
523
  * 그 순간 재고가 조용히 줄어든다(실제로 그랬다 — §ItemState.subLotId).
524
524
  *
525
525
  * ── 규약 (2026-08-21에 확정) ────────────────────────────────────────────────
@@ -20,7 +20,7 @@ function diffBy(predicted, actual, idOf, valOf) {
20
20
  }
21
21
  /** predicted(fork forecast)와 actual(관측) 스냅샷을 대조. 같은 sim 시점에 호출하는 것이 의미 있음. */
22
22
  export function compareStates(predicted, actual) {
23
- /* 키는 **한 규칙**으로 잡는다(`itemKeyOf`) — 같은 로트의 두 부분을 `epc` 로 접으면 발산 비교가
23
+ /* 키는 **한 규칙**으로 잡는다(`itemKeyOf`) — 같은 로트의 두 부분을 `epc` 로 합치면 발산 비교가
24
24
  "같은 물품이 두 곳에 있다" 를 자기 오류로 착각한다. */
25
25
  const itemLocation = diffBy(predicted.items, actual.items, itemKeyOf, i => i.location);
26
26
  const itemDisposition = diffBy(predicted.items, actual.items, itemKeyOf, i => i.disposition);
@@ -227,7 +227,7 @@ export const TWIN_RELATIONS = [
227
227
  * `series` 는 아직 **아무도 내지 않는다** — EMS 가 붙을 때 생산자와 함께 선언한다.
228
228
  */
229
229
  export const TWIN_PROPERTIES = [
230
- /* 공정 — 선언(ISO 8601 기간)과 관측(저널에서 접은 분포)이 둘 다 있는 유일한 짝. */
230
+ /* 공정 — 선언(ISO 8601 기간)과 관측(저널에서 계산한 분포)이 둘 다 있는 유일한 짝. */
231
231
  { axis: 'operations', key: 'duration', label: 'twin.prop.duration', kind: 'distribution',
232
232
  declaredField: 'duration', observedBy: 'kpi-fold.workTime', uom: 'ms' },
233
233
  /* 자리 — 용량은 선언, 점유는 관측. 단위가 다른 것이 아니라 **같은 축의 두 값**이다. */
@@ -59,7 +59,7 @@ export declare class EmsKernel extends FlowEngine {
59
59
  * **뿌리 계량기** — 조상 중에 계량된 자리가 없는 계량 지점들.
60
60
  *
61
61
  * 계층 계량(수전 ⊃ 분기)에서 전부 더하면 이중 계상이 된다. 뿌리만 더하면 현장의 실제 부하가 된다.
62
- * 모델이 그 계량기의 자리를 모르면 뿌리로 본다(모르는 것을 빼면 부하가 조용히 작아진다 — 그것이
62
+ * 모델이 그 계량기의 자리를 모르면 뿌리로 본다(모르는 것을 빼면 부하가 오류 없이 작아진다 — 그것이
63
63
  * 「계약 안쪽」이라는 더 위험한 거짓을 만든다).
64
64
  */
65
65
  private rootMeterIds;
@@ -91,7 +91,7 @@ export declare class EmsKernel extends FlowEngine {
91
91
  * "지금" 이 시뮬 기준시각(2026-01-01)에 얼어, 구간 마감·피크·감축 제안이 전부 8개월 전으로 기록됐다.
92
92
  *
93
93
  * 계측 자체는 원천 시각을 달고 있어서 화면의 순간값은 정상으로 보였다 — 그래서 조용한 결함이었다.
94
- * 저널을 기간으로 접는 쪽(성과·요금·피더 배분)은 그 트윈에서 영원히 아무것도 찾지 못했다.
94
+ * 저널을 기간으로 계산하는 쪽(성과·요금·피더 배분)은 그 트윈에서 영원히 아무것도 찾지 못했다.
95
95
  */
96
96
  apply(envelope: CanonicalEnvelope): void;
97
97
  /**
@@ -106,6 +106,26 @@ export declare class EmsKernel extends FlowEngine {
106
106
  *
107
107
  * 온 값만 덮는다: 보내지 않은 필드는 그대로 둔다(없는 값을 0 으로 만들지 않는다).
108
108
  */
109
+ /**
110
+ * **지금까지 만든 양**(적산)을 설비에 세운다 — 이 사건 이름은 오래 선언만 되어 있었다.
111
+ *
112
+ * ── 무엇이 없었나 (2026-08-26) ─────────────────────────────────────────────
113
+ * 설계 문서와 계약의 표에 `energy.generated` 가 있고 **내는 곳이 한 곳도 없었다.** 그래서 태양광
114
+ * 발전소를 붙인 현장에서 「지금 발전 중」은 보이고 「얼마나 발전했나」는 보이지 않았다(호현에너지
115
+ * 실측: 하루에 174kWh 가 자랐는데 트윈에는 없었다).
116
+ *
117
+ * ── 되돌아간 적산을 메우지 않는다 ──────────────────────────────────────────
118
+ * 인버터를 교체하면 적산이 0부터 다시 오른다. 그 계단을 우리가 메우면 **없던 발전량이 생긴다.** 그래서
119
+ * 값은 연결된 시스템이 준 대로 두고 「이 시각에 되돌아갔다」만 표시한다. 차분을 구하는 쪽이 그 시점을 건너
120
+ * 계산할 수 있어야 하고, 그 판단은 그쪽의 것이다.
121
+ *
122
+ * ── 늦게 온 옛 표본은 최신을 덮지 않는다 ───────────────────────────────────
123
+ * 계량 지점과 같은 규율이다(`point.atMs` 비교). 사건이 일어난 순서대로 도착하지 않으므로, 시각을 비교하지
124
+ * 않으면 늦게 온 옛 적산이 최신 값을 뒤로 돌린다 — 그리고 그것이 「계기가 되돌아갔다」로 잘못 적힌다.
125
+ */
126
+ private applyGenerated;
127
+ /** 설비마다 발전 적산을 마지막으로 들은 시각 — 늦게 온 옛 표본을 가리는 데만 쓴다. */
128
+ private generationHeardAtMs;
109
129
  private applyEquipmentEnergy;
110
130
  /** 우리 모델이 모르는 설비가 상태를 보내 온 횟수 — 조용히 버리지 않는다. */
111
131
  private unknownEquipmentReports;
@@ -206,16 +226,23 @@ export declare class EmsKernel extends FlowEngine {
206
226
  /** 자원 속성에서 수 하나 — 값이 수가 아니면 없는 것으로 본다(짐작하지 않는다). */
207
227
  private numberProperty;
208
228
  /** 글자로 선언된 값(발전 형상·통화 같은 것) — 비어 있으면 없는 것으로 본다. */
229
+ /**
230
+ * 이 계량 지점의 선언된 방향 — 자리와 설비 둘 다 본다(수전 자리에도, 계량기 설비에도 걸린다).
231
+ *
232
+ * 선언에 없는 낱말은 **받지 않는다**. 「outgoing」·「생산」처럼 뜻이 통할 것 같은 말을 받아 주면
233
+ * 현장마다 다른 어휘가 쌓이고, 그중 하나를 우리가 잘못 읽는 날 발전이 부하로 세어진다.
234
+ */
235
+ private meterDirectionOf;
209
236
  private stringProperty;
210
237
  tick(dtMs: number): void;
211
238
  /**
212
239
  * 에너지의 연속성 — **SCADA 는 「이번 15분에 지금까지 얼마」를 모른다.**
213
240
  *
214
241
  * 계량기는 순간값과 적산값을 준다. 「이 구간의 최대」·「구간이 열릴 때의 적산」·「관측 이후 최대」는
215
- * 우리가 접어 만든 값이라, 재기동하면 원천이 되풀어 주지 않는다. 잃으면 오류 없이 값이 작아진다 —
242
+ * 우리가 계산해 만든 값이라, 재기동하면 원천이 되풀어 주지 않는다. 잃으면 오류 없이 값이 작아진다 —
216
243
  * 구간 전력량이 기준점을 새로 잡아 짧아지고, 그 구간의 피크가 부팅 이후로만 잡힌다(요금이 걸린 수다).
217
244
  *
218
- * 지난 마감 구간도 이어받는다: 이미 저널에 있는 사실이지만, 화면이 그것을 보려고 매번 저널을 접지
245
+ * 지난 마감 구간도 이어받는다: 이미 저널에 있는 사실이지만, 화면이 그것을 보려고 매번 저널을 계산하지
219
246
  * 않게 한다(자른 사실은 `closedTotal` 이 그대로 나른다).
220
247
  *
221
248
  * 지금 열린 구간이 **이미 지나간 것**이어도 그대로 받는다 — 다음 표본이 오면 커널이 그 구간을 정상
@@ -28,7 +28,7 @@
28
28
  import { FlowEngine } from "./flow-engine.js";
29
29
  import { firstFitPolicy } from "./allocation-policy.js";
30
30
  import { CMD, ENERGY_EVENT, minuteOfDayAt } from "./contract.js";
31
- import { EMS_PROPERTY, electricalUpstreamOf, generationFractionAt } from "./ems-profile.js";
31
+ import { EMS_PROPERTY, METER_DIRECTION, electricalUpstreamOf, generationFractionAt } from "./ems-profile.js";
32
32
  /** 수요 구간 — 요금의 알갱이다. 15분은 한국·다수 요금제의 최대수요 산정 단위다. */
33
33
  export const DEMAND_WINDOW_MS = 15 * 60 * 1000;
34
34
  /** 그 시각이 속한 구간의 시작 — 벽시계 경계(00·15·30·45분)에 맞춘다. */
@@ -121,7 +121,7 @@ export class EmsKernel extends FlowEngine {
121
121
  * **뿌리 계량기** — 조상 중에 계량된 자리가 없는 계량 지점들.
122
122
  *
123
123
  * 계층 계량(수전 ⊃ 분기)에서 전부 더하면 이중 계상이 된다. 뿌리만 더하면 현장의 실제 부하가 된다.
124
- * 모델이 그 계량기의 자리를 모르면 뿌리로 본다(모르는 것을 빼면 부하가 조용히 작아진다 — 그것이
124
+ * 모델이 그 계량기의 자리를 모르면 뿌리로 본다(모르는 것을 빼면 부하가 오류 없이 작아진다 — 그것이
125
125
  * 「계약 안쪽」이라는 더 위험한 거짓을 만든다).
126
126
  */
127
127
  rootMeterIds() {
@@ -210,7 +210,7 @@ export class EmsKernel extends FlowEngine {
210
210
  * "지금" 이 시뮬 기준시각(2026-01-01)에 얼어, 구간 마감·피크·감축 제안이 전부 8개월 전으로 기록됐다.
211
211
  *
212
212
  * 계측 자체는 원천 시각을 달고 있어서 화면의 순간값은 정상으로 보였다 — 그래서 조용한 결함이었다.
213
- * 저널을 기간으로 접는 쪽(성과·요금·피더 배분)은 그 트윈에서 영원히 아무것도 찾지 못했다.
213
+ * 저널을 기간으로 계산하는 쪽(성과·요금·피더 배분)은 그 트윈에서 영원히 아무것도 찾지 못했다.
214
214
  */
215
215
  apply(envelope) {
216
216
  if (envelope.eventType === ENERGY_EVENT.measured) {
@@ -221,6 +221,10 @@ export class EmsKernel extends FlowEngine {
221
221
  this.applyEquipmentEnergy(envelope);
222
222
  return;
223
223
  }
224
+ if (envelope.eventType === ENERGY_EVENT.generated) {
225
+ this.applyGenerated(envelope);
226
+ return;
227
+ }
224
228
  super.apply(envelope);
225
229
  }
226
230
  /**
@@ -235,6 +239,58 @@ export class EmsKernel extends FlowEngine {
235
239
  *
236
240
  * 온 값만 덮는다: 보내지 않은 필드는 그대로 둔다(없는 값을 0 으로 만들지 않는다).
237
241
  */
242
+ /**
243
+ * **지금까지 만든 양**(적산)을 설비에 세운다 — 이 사건 이름은 오래 선언만 되어 있었다.
244
+ *
245
+ * ── 무엇이 없었나 (2026-08-26) ─────────────────────────────────────────────
246
+ * 설계 문서와 계약의 표에 `energy.generated` 가 있고 **내는 곳이 한 곳도 없었다.** 그래서 태양광
247
+ * 발전소를 붙인 현장에서 「지금 발전 중」은 보이고 「얼마나 발전했나」는 보이지 않았다(호현에너지
248
+ * 실측: 하루에 174kWh 가 자랐는데 트윈에는 없었다).
249
+ *
250
+ * ── 되돌아간 적산을 메우지 않는다 ──────────────────────────────────────────
251
+ * 인버터를 교체하면 적산이 0부터 다시 오른다. 그 계단을 우리가 메우면 **없던 발전량이 생긴다.** 그래서
252
+ * 값은 연결된 시스템이 준 대로 두고 「이 시각에 되돌아갔다」만 표시한다. 차분을 구하는 쪽이 그 시점을 건너
253
+ * 계산할 수 있어야 하고, 그 판단은 그쪽의 것이다.
254
+ *
255
+ * ── 늦게 온 옛 표본은 최신을 덮지 않는다 ───────────────────────────────────
256
+ * 계량 지점과 같은 규율이다(`point.atMs` 비교). 사건이 일어난 순서대로 도착하지 않으므로, 시각을 비교하지
257
+ * 않으면 늦게 온 옛 적산이 최신 값을 뒤로 돌린다 — 그리고 그것이 「계기가 되돌아갔다」로 잘못 적힌다.
258
+ */
259
+ applyGenerated(envelope) {
260
+ const d = envelope.data;
261
+ const id = String(d?.equipmentId ?? '').trim();
262
+ const eq = id ? this.equipment.get(id) : undefined;
263
+ if (!eq) {
264
+ this.unknownEquipmentReports++;
265
+ return;
266
+ }
267
+ const kWh = Number(d?.kWh);
268
+ if (!Number.isFinite(kWh))
269
+ return;
270
+ const atMs = [String(d?.at ?? ''), String(envelope?.eventTime ?? '')]
271
+ .map(v => Date.parse(v))
272
+ .find(v => Number.isFinite(v));
273
+ if (atMs === undefined)
274
+ return;
275
+ this.noteObserved(atMs);
276
+ /* 이 설비에 대해 마지막으로 들은 시각보다 오래된 것이면 값을 바꾸지 않는다. */
277
+ const heardAt = this.generationHeardAtMs.get(id);
278
+ if (heardAt !== undefined && atMs < heardAt)
279
+ return;
280
+ this.generationHeardAtMs.set(id, atMs);
281
+ const before = Number(eq.generatedKWh);
282
+ if (Number.isFinite(before) && kWh < before) {
283
+ /* 되돌아갔다 — 값은 새것으로 두고, 그 사실을 시각으로 남긴다(우리가 계단을 메우지 않는다). */
284
+ ;
285
+ eq.generatedKWhResetAt = new Date(atMs).toISOString();
286
+ }
287
+ ;
288
+ eq.generatedKWh = kWh;
289
+ eq.measuredAt = new Date(atMs).toISOString();
290
+ this.revision++;
291
+ }
292
+ /** 설비마다 발전 적산을 마지막으로 들은 시각 — 늦게 온 옛 표본을 가리는 데만 쓴다. */
293
+ generationHeardAtMs = new Map();
238
294
  applyEquipmentEnergy(envelope) {
239
295
  const d = envelope.data;
240
296
  const id = String(d?.equipmentId ?? '').trim();
@@ -300,6 +356,19 @@ export class EmsKernel extends FlowEngine {
300
356
  * 찍고 두면 나중에 선언된 계량기가 영원히 `observed` 로 남는다. 자리(`origin: 'observed'`)와 같은 어휘다.
301
357
  */
302
358
  point.origin = this.equipment.has(id) ? 'master' : 'observed';
359
+ /*
360
+ * ── 방향도 **표본마다 다시 맞춘다** (2026-08-25) ────────────────────────────
361
+ * `origin` 과 같은 이유다: 선언의 정본은 모델이고 모델은 채택으로 늘어난다. 한 번 찍고 두면
362
+ * 나중에 선언된 방향이 반영되지 않는다.
363
+ *
364
+ * 계량 지점은 자리일 수도 설비일 수도 있으므로 둘 다 본다(수전 자리에 계약이 걸리고, 계량기
365
+ * 설비에도 걸린다). 선언이 없으면 **칸을 만들지 않는다** — 「모른다」와 「쓰는 쪽이다」는 다르다.
366
+ */
367
+ const declaredDirection = this.meterDirectionOf(id);
368
+ if (declaredDirection)
369
+ point.direction = declaredDirection;
370
+ else
371
+ delete point.direction;
303
372
  /* 늦게 온 옛 표본이 최신 관측을 덮지 않게 — 상위 커널의 `stale` 판정과 같은 규율. */
304
373
  if (point.atMs === undefined || atMs >= point.atMs) {
305
374
  point.atMs = atMs;
@@ -341,7 +410,7 @@ export class EmsKernel extends FlowEngine {
341
410
  /*
342
411
  * ── 합을 **두 벌** 낸다 (2026-08-20) ──────────────────────────────────────
343
412
  *
344
- * 전부의 합(`total`)이 요금 판정의 근거다 — 모르는 계량기를 빼면 부하가 조용히 작아지고, 작아진
413
+ * 전부의 합(`total`)이 요금 판정의 근거다 — 모르는 계량기를 빼면 부하가 오류 없이 작아지고, 작아진
345
414
  * 부하는 「계약 안쪽」이라는 더 위험한 거짓을 만든다.
346
415
  *
347
416
  * 그런데 그 값 하나만 내면 반대쪽 거짓이 생긴다: 오타로 생긴 유령 계량기가 만든 초과를 화면이
@@ -352,16 +421,39 @@ export class EmsKernel extends FlowEngine {
352
421
  * 옛 스냅샷에서 이어받은 지점에는 그 칸이 없고, 없는 것을 「선언되었다」로 읽으면 이 값이 조용히
353
422
  * 전부의 합과 같아지기 때문이다.
354
423
  */
424
+ /*
425
+ * ── 내는 쪽은 수요에 더하지 않는다 (2026-08-25) ────────────────────────────
426
+ *
427
+ * **요금은 계통 접속점의 순 유입에 매겨진다.** 여기가 지점의 kW 를 전부 부하로 세고 있었으므로,
428
+ * 내보낸 양을 재는 계량기가 붙으면 그것이 수요에 더해지고 계약 대비 판단이 반대로 뒤집혔다.
429
+ * 상계 거래·축전지·열병합이 있는 곳에서는 접속점 계량기가 양쪽을 잰다.
430
+ *
431
+ * 세 갈래다.
432
+ * export 더하지 않고 `exportKW` 로 따로 센다. 빼지도 않는다(§`exportKW` 주석).
433
+ * bidirectional 그대로 더한다 — 그 계량기는 부호로 방향을 말한다(내보내는 동안 음수).
434
+ * 그 밖 · 선언 없음 더한다. 모르는 것을 빼면 부하가 오류 없이 작아진다.
435
+ */
355
436
  let total = 0;
356
437
  let declared = 0;
438
+ let exported = 0;
357
439
  for (const p of this.points.values()) {
358
440
  if (!roots.has(p.id))
359
441
  continue;
360
442
  const v = p.kW ?? 0;
443
+ if (p.direction === 'export') {
444
+ exported += v;
445
+ continue;
446
+ }
361
447
  total += v;
362
448
  if (this.equipment.has(p.id))
363
449
  declared += v;
364
450
  }
451
+ /*
452
+ * 내보낸 양은 **따로 최대를 잡는다** — 수요의 최대와 같은 순간이 아니다. 해가 가장 좋은 때와
453
+ * 부하가 가장 큰 때는 다르고, 한쪽 순간에 맞춰 다른 쪽을 적으면 두 수 다 사실이 아니게 된다.
454
+ */
455
+ if (exported > 0 && (w.exportKW === undefined || exported > w.exportKW))
456
+ w.exportKW = exported;
365
457
  if (w.maxKW === undefined || total > w.maxKW) {
366
458
  w.maxKW = total;
367
459
  w.maxDeclaredKW = declared;
@@ -451,6 +543,7 @@ export class EmsKernel extends FlowEngine {
451
543
  /* 총부하도 사실로 남긴다 — 이것이 없으면 저널을 읽는 쪽은 「무엇이 깎였나」를 되짚을 수 없다
452
544
  (요금은 순수요로 매겨지지만, 그 수요를 만든 것은 설비 부하다). */
453
545
  ...(w.grossMaxKW !== undefined ? { grossMaxKW: w.grossMaxKW } : {}),
546
+ ...(w.exportKW !== undefined ? { exportKW: w.exportKW } : {}),
454
547
  ...(w.meanKW !== undefined ? { meanKW: w.meanKW } : {}),
455
548
  samples: w.samples,
456
549
  ...(w.contractKW !== undefined ? { contractKW: w.contractKW } : {}),
@@ -467,7 +560,7 @@ export class EmsKernel extends FlowEngine {
467
560
  * ── 지점별 몫을 함께 낸다 (2026-08-18) ────────────────────────────────
468
561
  *
469
562
  * 분기마다 계량기가 있는 현장에서 분기별 전력량은 **측정**이다. 그런데 그 사실이 저널에
470
- * 없어서, 저널을 접는 쪽은 가동시간으로 **배분**할 수밖에 없었다 — 계량이 있는데 배분으로
563
+ * 없어서, 저널을 계산하는 쪽은 가동시간으로 **배분**할 수밖에 없었다 — 계량이 있는데 배분으로
471
564
  * 말하는 것은 근거를 실제보다 약하게 보고하는 것이고, 그 위에 세운 SEU 판단이 무거운 값을 놓친다.
472
565
  *
473
566
  * 표본을 그대로 저널에 흘리는 대신 **구간마다 한 줄**로 낸다: 계측은 초당 여러 건이라 표본을
@@ -899,6 +992,20 @@ export class EmsKernel extends FlowEngine {
899
992
  return undefined;
900
993
  }
901
994
  /** 글자로 선언된 값(발전 형상·통화 같은 것) — 비어 있으면 없는 것으로 본다. */
995
+ /**
996
+ * 이 계량 지점의 선언된 방향 — 자리와 설비 둘 다 본다(수전 자리에도, 계량기 설비에도 걸린다).
997
+ *
998
+ * 선언에 없는 낱말은 **받지 않는다**. 「outgoing」·「생산」처럼 뜻이 통할 것 같은 말을 받아 주면
999
+ * 현장마다 다른 어휘가 쌓이고, 그중 하나를 우리가 잘못 읽는 날 발전이 부하로 세어진다.
1000
+ */
1001
+ meterDirectionOf(id) {
1002
+ /* 선언의 정본은 모델이다(`boardDef`) — 실행 중 상태(`this.locations`)에는 속성이 없다. */
1003
+ const declared = this.stringProperty((this.boardDef?.equipment ?? []).find(e => e.id === id), EMS_PROPERTY.meterDirection) ??
1004
+ this.stringProperty((this.boardDef?.locations ?? []).find(l => l.id === id), EMS_PROPERTY.meterDirection);
1005
+ if (!declared)
1006
+ return undefined;
1007
+ return METER_DIRECTION.includes(declared) ? declared : undefined;
1008
+ }
902
1009
  stringProperty(resource, id) {
903
1010
  for (const p of resource?.properties ?? []) {
904
1011
  if (p?.id !== id)
@@ -929,10 +1036,10 @@ export class EmsKernel extends FlowEngine {
929
1036
  * 에너지의 연속성 — **SCADA 는 「이번 15분에 지금까지 얼마」를 모른다.**
930
1037
  *
931
1038
  * 계량기는 순간값과 적산값을 준다. 「이 구간의 최대」·「구간이 열릴 때의 적산」·「관측 이후 최대」는
932
- * 우리가 접어 만든 값이라, 재기동하면 원천이 되풀어 주지 않는다. 잃으면 오류 없이 값이 작아진다 —
1039
+ * 우리가 계산해 만든 값이라, 재기동하면 원천이 되풀어 주지 않는다. 잃으면 오류 없이 값이 작아진다 —
933
1040
  * 구간 전력량이 기준점을 새로 잡아 짧아지고, 그 구간의 피크가 부팅 이후로만 잡힌다(요금이 걸린 수다).
934
1041
  *
935
- * 지난 마감 구간도 이어받는다: 이미 저널에 있는 사실이지만, 화면이 그것을 보려고 매번 저널을 접지
1042
+ * 지난 마감 구간도 이어받는다: 이미 저널에 있는 사실이지만, 화면이 그것을 보려고 매번 저널을 계산하지
936
1043
  * 않게 한다(자른 사실은 `closedTotal` 이 그대로 나른다).
937
1044
  *
938
1045
  * 지금 열린 구간이 **이미 지나간 것**이어도 그대로 받는다 — 다음 표본이 오면 커널이 그 구간을 정상
@@ -1,6 +1,24 @@
1
1
  import type { TwinTypeInfo } from './domain-catalog.ts';
2
2
  /** 로케이션(수동) 타입 키 — 배전 계통의 구간. */
3
3
  export declare const EMS_LOCATION_TYPES: readonly ["incoming", "feeder", "submeter-zone"];
4
+ /**
5
+ * 계량 지점의 방향 — **무엇을 재는 계량기인가.**
6
+ *
7
+ * import 쓰는 쪽. 계통에서 받는다(대부분의 계량기).
8
+ * export 내는 쪽. 발전이 계통으로 나가는 것을 잰다.
9
+ * bidirectional 양쪽을 한 계량기로 잰다(상계 거래·축전지). 부호가 방향을 담는다.
10
+ *
11
+ * 선언하지 않으면 **모르는 것**이고, 모르는 것은 부하로 센다. 빼면 부하가 오류 없이 작아지고, 작아진
12
+ * 부하는 「계약 안쪽」이라는 더 위험한 거짓을 만든다(같은 이유로 정체 모를 계량기도 합에 넣는다).
13
+ *
14
+ * `bidirectional` 은 **그 계량기가 순 유입을 재는 것**이다. 발전을 부하에서 빼는 것이 아니다 — 그
15
+ * 계량기가 읽은 값이 그 접속점의 사실이고, 요금이 그 값에 매겨진다.
16
+ *
17
+ * 표준 근거: 계량기의 적산 레지스터가 유입·유출로 갈려 있고(IEC 62053 계열), ISO 50001 은 유입 경계를
18
+ * 따로 둔다(`EnergyInput`). 우리가 지은 낱말이 아니다.
19
+ */
20
+ export declare const METER_DIRECTION: readonly ["import", "export", "bidirectional"];
21
+ export type MeterDirection = (typeof METER_DIRECTION)[number];
4
22
  /** 설비(능동) 타입 키 — 계량 지점과 에너지 자원. */
5
23
  export declare const EMS_EQUIPMENT_TYPES: readonly ["meter", "breaker", "pv-array", "battery", "utility", "curtailable-load"];
6
24
  /**
@@ -12,6 +30,7 @@ export declare const EMS_EQUIPMENT_TYPES: readonly ["meter", "breaker", "pv-arra
12
30
  export declare const EMS_PROPERTY: {
13
31
  /** 계약전력(kW) — 수전·분기 자리에 선언한다. 없으면 계약 대비 판정을 하지 않는다. */
14
32
  readonly contractKW: "contract.kW";
33
+ readonly meterDirection: "meter.direction";
15
34
  /** 가동 중 소비(kW). */
16
35
  readonly ratedKW: "power.ratedKW";
17
36
  /** 멈춰 있을 때의 소비(kW). 없으면 멈춘 동안을 **비운다** — 0 이라고 주장하지 않는다. */
@@ -1,5 +1,22 @@
1
1
  /** 로케이션(수동) 타입 키 — 배전 계통의 구간. */
2
2
  export const EMS_LOCATION_TYPES = ['incoming', 'feeder', 'submeter-zone'];
3
+ /**
4
+ * 계량 지점의 방향 — **무엇을 재는 계량기인가.**
5
+ *
6
+ * import 쓰는 쪽. 계통에서 받는다(대부분의 계량기).
7
+ * export 내는 쪽. 발전이 계통으로 나가는 것을 잰다.
8
+ * bidirectional 양쪽을 한 계량기로 잰다(상계 거래·축전지). 부호가 방향을 담는다.
9
+ *
10
+ * 선언하지 않으면 **모르는 것**이고, 모르는 것은 부하로 센다. 빼면 부하가 오류 없이 작아지고, 작아진
11
+ * 부하는 「계약 안쪽」이라는 더 위험한 거짓을 만든다(같은 이유로 정체 모를 계량기도 합에 넣는다).
12
+ *
13
+ * `bidirectional` 은 **그 계량기가 순 유입을 재는 것**이다. 발전을 부하에서 빼는 것이 아니다 — 그
14
+ * 계량기가 읽은 값이 그 접속점의 사실이고, 요금이 그 값에 매겨진다.
15
+ *
16
+ * 표준 근거: 계량기의 적산 레지스터가 유입·유출로 갈려 있고(IEC 62053 계열), ISO 50001 은 유입 경계를
17
+ * 따로 둔다(`EnergyInput`). 우리가 지은 낱말이 아니다.
18
+ */
19
+ export const METER_DIRECTION = ['import', 'export', 'bidirectional'];
3
20
  /** 설비(능동) 타입 키 — 계량 지점과 에너지 자원. */
4
21
  export const EMS_EQUIPMENT_TYPES = ['meter', 'breaker', 'pv-array', 'battery', 'utility', 'curtailable-load'];
5
22
  /**
@@ -11,6 +28,24 @@ export const EMS_EQUIPMENT_TYPES = ['meter', 'breaker', 'pv-array', 'battery', '
11
28
  export const EMS_PROPERTY = {
12
29
  /** 계약전력(kW) — 수전·분기 자리에 선언한다. 없으면 계약 대비 판정을 하지 않는다. */
13
30
  contractKW: 'contract.kW',
31
+ /*
32
+ * ── 계량 지점의 방향 (2026-08-25) ──────────────────────────────────────────
33
+ * **요금은 계통 접속점의 순 유입에 매겨진다.** 그러니 그 지점의 계량기가 무엇을 재는지 모르면
34
+ * 요금을 사실대로 말할 수 없다 — 내보낸 양을 쓴 양으로 세면 계약 대비 판단이 반대로 뒤집힌다.
35
+ *
36
+ * 이 축이 없는 동안 계량 지점의 kW 를 전부 부하로 셌다. 상계 거래를 하는 현장, 축전지를 붙인 현장,
37
+ * 열병합이 있는 현장 — 접속점 계량기가 양쪽을 재는 곳은 늘 있다.
38
+ *
39
+ * **발전기를 계량기로 만드는 것과 다른 일이다.** 발전은 설비의 사실이고(`energyGenerating`), 그것을
40
+ * 계량 지점으로 보내는 것은 여전히 하지 않는다. 이 축은 **접속점 계량기**가 무엇을 재는지 말하는
41
+ * 자리다.
42
+ *
43
+ * 방향은 **관측이 아니라 선언**이다. 표본마다 바뀌는 값이 아니고, 그 계량기가 무엇을 재도록
44
+ * 설치되었는지는 현장이 안다. 그래서 표본(`EnergyRecord`)에 넣지 않고 자원 속성으로 둔다.
45
+ *
46
+ * 값은 계량기 자신의 낱말을 쓴다(적산 레지스터가 그렇게 갈려 있다). §`METER_DIRECTION`.
47
+ */
48
+ meterDirection: 'meter.direction',
14
49
  /*
15
50
  * ── 부하 계수 — 시뮬레이션이 전기를 만들 수 있게 (2026-08-14) ───────────────
16
51
  * 「이 설비가 돌면 몇 kW 인가」는 **현장이 아는 값**이다. 그래서 타입 기본값을 두지 않는다:
@@ -96,6 +131,13 @@ export const EMS_PROPERTY = {
96
131
  };
97
132
  export const EMS_PROPERTY_SPEC = {
98
133
  [EMS_PROPERTY.contractKW]: { uom: 'kW', dataType: 'xs:double', note: 'contracted power at the metering point, in kW.' },
134
+ /* 값이 셋 중 하나여야 한다 — 그 밖의 낱말은 받지 않는다(뜻이 통할 것 같은 말도 받지 않는다). */
135
+ [EMS_PROPERTY.meterDirection]: {
136
+ dataType: 'xs:string',
137
+ note: 'what this metering point measures: "import" (drawn from the grid), "export" (generation sent out), ' +
138
+ 'or "bidirectional" (one meter for both, sign carries the direction). ' +
139
+ 'Left undeclared, the point counts as load — leaving it out never makes demand look smaller than it is.'
140
+ },
99
141
  [EMS_PROPERTY.ratedKW]: { uom: 'kW', dataType: 'xs:double', note: 'power drawn while running, in kW.' },
100
142
  [EMS_PROPERTY.standbyKW]: { uom: 'kW', dataType: 'xs:double', note: 'power drawn while idle, in kW.' },
101
143
  [EMS_PROPERTY.demandChargePerKW]: { dataType: 'xs:double', note: 'demand charge per kW of billing-period peak, in the declared currency.' },
@@ -53,10 +53,27 @@ export interface EnergyEquipmentRecord {
53
53
  /** 이 레코드가 설비 에너지 상태인가 — 라우팅 판정을 한 곳에 둔다. */
54
54
  export declare function isEnergyEquipmentRecord(record: unknown): boolean;
55
55
  /**
56
- * 설비 에너지 레코드 → 봉투. 계량과 같은 규율이다: 지어낼 수 없는 것이 빠지면 거부하고, 거부한 것은
57
- * 이유와 함께 돌려준다.
56
+ * 설비 에너지 레코드 → 봉투. 계량과 같은 규율이다: 지어낼 수 없는 것이 빠지면 받지 않고, 받지 않은 것은
57
+ * 이유와 함께 알린다.
58
58
  *
59
59
  * **값이 하나도 없는 레코드는 거부한다** — 계량과 다른 점이다. 계량은 「응답했다」는 사실 자체가
60
60
  * 관측이지만(그래서 kW 없는 표본을 받는다), 여기서는 바꿀 상태가 없는 것을 사실로 적을 이유가 없다.
61
61
  */
62
62
  export declare function ingestEnergyEquipmentRecords(records: EnergyEquipmentRecord | EnergyEquipmentRecord[] | undefined | null, opts: EnergyIngestOptions): EnergyIngestResult;
63
+ /** 정규 발전 적산 레코드 — 커넥터가 이 모양으로 맞춰 준다(필드 이름이 계약이다). */
64
+ export interface EnergyGenerationRecord {
65
+ equipmentId: string;
66
+ /** 계기 적산값(kWh) — 연결된 시스템이 준 값 그대로. 차분은 소비처가 한다. */
67
+ kWh?: number;
68
+ at?: string;
69
+ }
70
+ /** 이 레코드가 발전 적산인가 — 어느 갈래로 보낼지를 한 곳에서 정한다. */
71
+ export declare function isEnergyGenerationRecord(record: unknown): boolean;
72
+ /**
73
+ * 발전 적산 레코드 → 봉투. 계량과 같은 규율이다: 지어낼 수 없는 것이 빠지면 받지 않고, 받지 않은 것은
74
+ * 이유와 함께 알린다.
75
+ *
76
+ * **적산이 없는 레코드는 받지 않는다** — 이 문의 값은 그것 하나다. 순시 출력은 설비 상태 문이 받는다
77
+ * (`EnergyEquipmentRecord.generatedKW`).
78
+ */
79
+ export declare function ingestEnergyGenerationRecords(records: EnergyGenerationRecord | EnergyGenerationRecord[] | undefined | null, opts: EnergyIngestOptions): EnergyIngestResult;