@operato/twin-kernel 0.7.65 → 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.
@@ -1,5 +1,5 @@
1
1
  import type { IngestResult } from './face2-adapter.ts';
2
- /** 이 문이 받는 여섯 가지 — 리듀서가 접는 것과 같은 목록(주목 확인은 우리 안의 행위라 제외). */
2
+ /** 이 문이 받는 여섯 가지 — 리듀서가 다루는 것과 같은 목록(주목 확인은 우리 안의 행위라 제외). */
3
3
  export type OperationalKind = 'task' | 'equipment' | 'person' | 'asset' | 'order' | 'quality' | 'test' | 'observation' | 'complete';
4
4
  /**
5
5
  * 정규 운영 레코드 — **델타의 필드 이름 + 시각(`at`)**.
@@ -2,7 +2,7 @@
2
2
  * 운영 사실 인제스트 — **작업·설비·사람·자산·오더·품질이 들어오는 문.** (ADR-0029 어휘 넓히기)
3
3
  *
4
4
  * ── 무엇이 없었나 (2026-08-19) ──────────────────────────────────────────────
5
- * 관측 리듀서는 이 여섯을 **이미 접는다**(`observed-reducer.ts` 의 `OP_EVENT.*` 분기). 그런데 라이브
5
+ * 관측 리듀서는 이 여섯을 **이미 다룬다**(`observed-reducer.ts` 의 `OP_EVENT.*` 분기). 그런데 라이브
6
6
  * 인제스트 문은 어휘를 셋만 알았다: EPCIS 품목 사실, 에너지 계량, 설비 에너지 상태. 그래서 원본이
7
7
  * 「이 작업이 끝났다」·「이 설비가 고장이다」를 말할 **길이 없었다** — 넣으면 `epc` 가 없어 EPCIS
8
8
  * 검증에서 거부됐다.
@@ -17,14 +17,14 @@
17
17
  * 규율을 먼저 세웠다: "필드 이름이 계약이다").
18
18
  *
19
19
  * ── 무엇을 거부하나 ─────────────────────────────────────────────────────────
20
- * 지어낼 수 없는 것이 빠지면 거부한다 — 정체(누구의 상태인가)와 상태다. 그리고 **접을 수 없는 낱말**도
20
+ * 지어낼 수 없는 것이 빠지면 거부한다 — 정체(누구의 상태인가)와 상태다. 그리고 **계산할 수 없는 낱말**도
21
21
  * 거부한다: 설비 상태를 `'RUNNING'` 으로 받으면 아무 오류 없이 가동률이 0% 가 되고(누적기는 `busy`·
22
22
  * `down` 만 센다), 사람이 `'available'` 이면 배정에서 조용히 사라진다. 그 실패는 화면에서 「일이 없는
23
- * 공장」으로 보이고 원인을 되짚을 수 없다. 그래서 커널이 접을 수 있는 낱말만 받고, **받는 낱말을 이유에
23
+ * 공장」으로 보이고 원인을 되짚을 수 없다. 그래서 커널이 다룰 수 있는 낱말만 받고, **받는 낱말을 이유에
24
24
  * 적어** 커넥터가 매핑을 고칠 수 있게 한다(매핑=밖, 검증=커널).
25
25
  *
26
26
  * 오더의 상태·종류는 **열려 있다** — 도메인이 소유한다(`picking`·`packed`·`shipped`…). 커널이 그 낱말로
27
- * 무엇을 접지 않으므로 닫을 근거가 없다.
27
+ * 무엇을 계산하지 않으므로 닫을 근거가 없다.
28
28
  *
29
29
  * ── 파생은 받아도 커널이 다시 계산한다 ──────────────────────────────────────
30
30
  * 작업의 진척(`progress`)은 계약에 있어 받지만, 상태에 앉는 값은 커널이 **소요·남은 시간에서 다시
@@ -49,7 +49,7 @@ const SPECS = {
49
49
  task: {
50
50
  eventType: OP_EVENT.task,
51
51
  identity: 'taskId',
52
- /* 종류가 없으면 성과를 종류별로 접을 수 없고(선언된 시간·수율이 종류로 붙는다) 지어낼 수도 없다. */
52
+ /* 종류가 없으면 성과를 종류별로 모을 수 없고(선언된 시간·수율이 종류로 붙는다) 지어낼 수도 없다. */
53
53
  required: ['taskId', 'kind', 'status'],
54
54
  fields: {
55
55
  taskId: 'string', kind: 'string', status: 'string', fromNode: 'string', toNode: 'string',
@@ -296,7 +296,7 @@ export function ingestOperationalRecords(records, opts) {
296
296
  }
297
297
  const allowed = spec.enums?.[name];
298
298
  if (allowed && !allowed.includes(v)) {
299
- errors.push(`${kind}.${name} 이 커널이 접는 낱말이 아니다: ${JSON.stringify(v)} — 받는 값은 ${allowed.join(' · ')}`);
299
+ errors.push(`${kind}.${name} 이 커널이 다루는 낱말이 아니다: ${JSON.stringify(v)} — 받는 값은 ${allowed.join(' · ')}`);
300
300
  break;
301
301
  }
302
302
  data[name] = v;
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  * `StateProjector` — **옛 이름.** 규칙 자체는 `observed-reducer.ts` 로 옮겼다.
3
3
  *
4
- * 이름을 옮긴 이유: 이 규칙은 이제 미러 전용이 아니다. 커널도 같은 규칙으로 이벤트를 접는다
4
+ * 이름을 옮긴 이유: 이 규칙은 이제 미러 전용이 아니다. 커널도 같은 규칙으로 이벤트를 계산한다
5
5
  * (`FlowEngine.apply`). "투영기" 라는 이름은 미러만의 것처럼 읽혀 두 구동이 규칙을 나눠 갖는 구조를
6
6
  * 가린다. 소비처(호스트·테스트)가 많아 이름은 그대로 쓸 수 있게 남긴다.
7
7
  */
@@ -28,7 +28,7 @@ export interface TaskFacets {
28
28
  *
29
29
  * 이 축이 없으면 「이 공정의 양품률이 얼마였나」를 저널에서 되짚을 수 없다: 양품/불량은 설비 누적
30
30
  * 카운터(종류 모름)와 EPCIS disposition(종류 모름)에만 남아 있었다. 판정은 작업 완료에서 일어나므로
31
- * 그 자리에서 적고, 여기서 그대로 접는다.
31
+ * 그 자리에서 적고, 여기서 그대로 처리한다.
32
32
  *
33
33
  * **없음은 「양품」이 아니라 「판정하지 않았다」**다(이동·체류에는 품질 판정이 없다).
34
34
  */
@@ -54,7 +54,7 @@ export interface TaskFoldResult {
54
54
  latestMs: number;
55
55
  }
56
56
  /**
57
- * `task.status` 델타를 작업 단위 기록으로 접는다.
57
+ * `task.status` 델타를 작업 단위 기록으로 처리한다.
58
58
  *
59
59
  * 입력은 **시간순일 필요가 없다**(revision 순·역순 모두 허용) — 이정표는 min/max 로 고르고 축의 값은
60
60
  * 처음 본 것을 남기므로 순서에 의존하지 않는다. 단 `createdMs` 는 "마지막 생성 전이" 이므로 같은 작업의
package/dist/task-fold.js CHANGED
@@ -10,7 +10,7 @@ function deltaOf(row) {
10
10
  return (row.data ?? p?.data ?? p ?? {});
11
11
  }
12
12
  /**
13
- * `task.status` 델타를 작업 단위 기록으로 접는다.
13
+ * `task.status` 델타를 작업 단위 기록으로 처리한다.
14
14
  *
15
15
  * 입력은 **시간순일 필요가 없다**(revision 순·역순 모두 허용) — 이정표는 min/max 로 고르고 축의 값은
16
16
  * 처음 본 것을 남기므로 순서에 의존하지 않는다. 단 `createdMs` 는 "마지막 생성 전이" 이므로 같은 작업의
@@ -161,7 +161,7 @@ export class YmsKernel extends FlowEngine {
161
161
  /** 태스크 완료 — 목적지 타입으로 분기: yard-slot=주차, dock-door=상/하차+depart, gate=출차. */
162
162
  onTaskComplete(t) {
163
163
  const trailer = this.itemByRef(t.itemEpc);
164
- /* 여기 도달했다면 코어가 이미 물품을 확인했다(주체가 사라진 작업은 완료 전에 접힌다).
164
+ /* 여기 도달했다면 코어가 이미 물품을 확인했다(주체가 사라진 작업은 완료 전에 제외된다).
165
165
  그래도 단정(`!`)은 쓰지 않는다 — 계약이 바뀌면 조용히 틀리는 대신 분명히 멈춘다. */
166
166
  if (!trailer)
167
167
  throw new Error(`task ${t.id}: item "${t.itemEpc}" vanished between the core check and the domain hook`);
@@ -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` 로 접지 않나 (2026-08-24) ─────────────────────────────
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` 는 **날짜**이고 이것은 **상태**다. 날짜가 있으면 「지났나」는 파생이지만, 원본이
@@ -1458,12 +1461,12 @@ var ObservedReducer = class {
1458
1461
  /**
1459
1462
  * **구조를 전환한다** — 관측된 사실은 지키고 토폴로지만 새 선언으로 바꾼다.
1460
1463
  *
1461
- * 공장은 바뀐다. 도장 부스를 넷 더 놓고, 라인을 하나 접는다. 그런데 지금까지는 구조가 바뀌면
1464
+ * 공장은 바뀐다. 도장 부스를 넷 더 놓고, 라인 하나를 닫는다. 그런데 지금까지는 구조가 바뀌면
1462
1465
  * **그 트윈의 저널을 통째로 지우는 것**이 유일한 길이었다 — 안 지우면 옛 이벤트를 새 공장에 대고
1463
- * 접게 되어 이력이 거짓말을 한다. 역사를 잃거나 거짓말을 하거나, 둘뿐이었다.
1466
+ * 계산하게 되어 이력이 거짓말을 한다. 역사를 잃거나 거짓말을 하거나, 둘뿐이었다.
1464
1467
  *
1465
1468
  * 셋째 길이 이것이다: 이벤트가 **자기 구조를 달고** 다니고, 재생은 구조가 바뀌는 지점에서 여기를
1466
- * 불러 전환한 뒤 이어 접는다. 그러면 "그때 그 공장의 사실" 로 계속 읽힌다.
1469
+ * 불러 전환한 뒤 이어서 계산한다. 그러면 "그때 그 공장의 사실" 로 계속 읽힌다.
1467
1470
  *
1468
1471
  * ── 무엇을 지키고 무엇을 버리는가 ───────────────────────────────────────
1469
1472
  * **관측은 지킨다** — 물품·오더·작업·집합은 구조와 무관한 사실이다(팔레트는 부스를 늘려도 그대로다).
@@ -2028,7 +2031,7 @@ var ObservedReducer = class {
2028
2031
  * 시뮬 기준시각(BASE_EPOCH, 2026-01-01)에 얼어 있었다.
2029
2032
  *
2030
2033
  * 결과는 조용했다: 구간 마감·피크 같은 **커널이 낸 사실이 8개월 전으로 기록**되어, 저널을 기간으로
2031
- * 접는 소비처(성과·요금·피더 배분)가 그 트윈에서 영원히 아무것도 찾지 못했다. 계측 자체는 원천 시각을
2034
+ * 계산하는 소비처(성과·요금·피더 배분)가 그 트윈에서 영원히 아무것도 찾지 못했다. 계측 자체는 원천 시각을
2032
2035
  * 달고 있었으므로 화면의 순간값만 정상으로 보였다 — 그래서 눈에 띄지 않았다.
2033
2036
  *
2034
2037
  * 시각을 되돌리지는 않는다: 늦게 도착한 옛 표본이 "지금" 을 과거로 끌면 그 뒤 판정이 전부 흔들린다.
@@ -2087,20 +2090,20 @@ var ObservedReducer = class {
2087
2090
  /**
2088
2091
  * **재개점(checkpoint)** — 스냅샷과 다르다.
2089
2092
  *
2090
- * ── 왜 스냅샷으로는 이어 접을 수 없나 (2026-08-18) ─────────────────────────
2093
+ * ── 왜 스냅샷으로는 이어 계산할 수 없나 (2026-08-18) ─────────────────────────
2091
2094
  * `snapshot()` 은 **소비처가 보는 값**이다(파생된 판정·정리된 목록). 그것으로 리듀서를 되세우면
2092
2095
  * 보이지 않는 것들이 사라진다 — 아직 등장하지 않은 자식의 담김(`pendingParent`), 집합 관계
2093
2096
  * (`aggregation`), 반영 못 한 사건 집계(`unhandled`), 정정 선언(`corrections`). 그 상태에서 뒤 이벤트를
2094
- * 접으면 **0부터 접은 결과와 달라진다.** 다르면 그 차이는 조용하다(오류가 없다).
2097
+ * 계산하면 **0부터 계산한 결과와 달라진다.** 다르면 그 차이는 조용하다(오류가 없다).
2095
2098
  *
2096
- * 그래서 재개점은 **내부 상태 전부**다. 「이어 접기」의 정합성은 시험이 증명한다:
2097
- * 0부터 접은 결과 == 앞부분 재개점 + 뒷부분 접기.
2099
+ * 그래서 재개점은 **내부 상태 전부**다. 「이어 계산」의 정합성은 시험이 증명한다:
2100
+ * 0부터 계산한 결과 == 앞부분 재개점 + 뒷부분 계산.
2098
2101
  *
2099
2102
  * 모델에서 오는 것(교대·등급·시간대)은 담지 않는다 — 되세울 때 같은 모델을 받기 때문이다.
2100
2103
  */
2101
2104
  serialize() {
2102
2105
  return {
2103
- /* 관측도 재개점에 든다 — 없으면 이어 접기가 그 축을 0부터 다시 만든다. */
2106
+ /* 관측도 재개점에 든다 — 없으면 이어 계산가 그 축을 0부터 다시 만든다. */
2104
2107
  observations: [...this.observations.entries()].map(([id, bin]) => ({ id, values: [...bin.values()] })),
2105
2108
  revision: this.revision,
2106
2109
  master: [...this.master.values()].map((n) => ({ ...n })),
@@ -2282,10 +2285,29 @@ var WMS_TYPES = [
2282
2285
 
2283
2286
  // src/ems-profile.ts
2284
2287
  var EMS_LOCATION_TYPES = ["incoming", "feeder", "submeter-zone"];
2288
+ var METER_DIRECTION = ["import", "export", "bidirectional"];
2285
2289
  var EMS_EQUIPMENT_TYPES = ["meter", "breaker", "pv-array", "battery", "utility", "curtailable-load"];
2286
2290
  var EMS_PROPERTY = {
2287
2291
  /** 계약전력(kW) — 수전·분기 자리에 선언한다. 없으면 계약 대비 판정을 하지 않는다. */
2288
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",
2289
2311
  /*
2290
2312
  * ── 부하 계수 — 시뮬레이션이 전기를 만들 수 있게 (2026-08-14) ───────────────
2291
2313
  * 「이 설비가 돌면 몇 kW 인가」는 **현장이 아는 값**이다. 그래서 타입 기본값을 두지 않는다:
@@ -2371,6 +2393,11 @@ var EMS_PROPERTY = {
2371
2393
  };
2372
2394
  var EMS_PROPERTY_SPEC = {
2373
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
+ },
2374
2401
  [EMS_PROPERTY.ratedKW]: { uom: "kW", dataType: "xs:double", note: "power drawn while running, in kW." },
2375
2402
  [EMS_PROPERTY.standbyKW]: { uom: "kW", dataType: "xs:double", note: "power drawn while idle, in kW." },
2376
2403
  [EMS_PROPERTY.demandChargePerKW]: { dataType: "xs:double", note: "demand charge per kW of billing-period peak, in the declared currency." },
@@ -2628,7 +2655,15 @@ var CAPABILITIES = {
2628
2655
  label: "twin.capability.energyGenerating",
2629
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.",
2630
2657
  commands: [],
2631
- stateFields: ["generatedKW", "exportKW"],
2658
+ /*
2659
+ * ── 적산을 더한다 (2026-08-26) ─────────────────────────────────────────────
2660
+ * 이 능력이 순시 출력만 선언하고 있었다. 그래서 「지금 발전 중」은 말하고 「얼마나 발전했나」는 말할
2661
+ * 수 없었다 — 사건 이름(`energy.generated`)과 설계 문서에는 처음부터 있던 사실이다.
2662
+ *
2663
+ * 계량 능력(`metered`)이 `kW` 와 `kWh` 를 함께 선언한 것과 같은 짝이다. 적산은 발전 설비의 보편적
2664
+ * 사실이고(태양광·열병합·디젤), 그것이 없으면 성능비·발전시간 같은 성과를 아무도 셀 수 없다.
2665
+ */
2666
+ stateFields: ["generatedKW", "exportKW", "generatedKWh", "generatedKWhResetAt"],
2632
2667
  models: ["GeneratingState"],
2633
2668
  results: ["generated"]
2634
2669
  },
@@ -3047,7 +3082,7 @@ var TWIN_RELATIONS = [
3047
3082
  { from: "operations", field: "resourceType", target: { kind: "type", role: "equipment" }, via: "twin.rel.by", optional: true }
3048
3083
  ];
3049
3084
  var TWIN_PROPERTIES = [
3050
- /* 공정 — 선언(ISO 8601 기간)과 관측(저널에서 접은 분포)이 둘 다 있는 유일한 짝. */
3085
+ /* 공정 — 선언(ISO 8601 기간)과 관측(저널에서 계산한 분포)이 둘 다 있는 유일한 짝. */
3051
3086
  {
3052
3087
  axis: "operations",
3053
3088
  key: "duration",
@@ -3905,10 +3940,14 @@ function computeOee(c, nowMs) {
3905
3940
  scrapCount: c.scrapCount
3906
3941
  };
3907
3942
  }
3943
+ var ENERGY_STATE_FIELDS = [
3944
+ ...stateFieldsOf(["metered", "curtailable", "energyGenerating", "energyStoring", "switching"]),
3945
+ "measuredAt"
3946
+ ];
3908
3947
  function energyFieldsOf(m) {
3909
3948
  const out = {};
3910
3949
  const src = m;
3911
- for (const k of ["generatedKW", "exportKW", "soc", "chargeKW", "dischargeKW", "curtailable", "minKW", "position", "measuredAt"]) {
3950
+ for (const k of ENERGY_STATE_FIELDS) {
3912
3951
  if (src[k] !== void 0) out[k] = src[k];
3913
3952
  }
3914
3953
  return out;
@@ -4131,7 +4170,7 @@ var FlowEngine = class {
4131
4170
  * 양품률 시임(선택) — **이력에서 배운 수율.** 소요시간과 같은 자리·같은 규율이다 (2026-08-19).
4132
4171
  *
4133
4172
  * 없으면 선언값(현장→원천), 그것도 없으면 도메인 상수. 무엇을 썼는지는 `specCoverage()` 가 밝힌다.
4134
- * 주입은 호스트가 한다(저널을 접는 쪽이 이력을 안다) — 커널은 시임만 든다.
4173
+ * 주입은 호스트가 한다(저널을 계산하는 쪽이 이력을 안다) — 커널은 시임만 든다.
4135
4174
  */
4136
4175
  yieldEstimator;
4137
4176
  /**
@@ -4153,7 +4192,7 @@ var FlowEngine = class {
4153
4192
  * 읽는 쪽(`paramNumber`·`paramDuration`)이 명세 행보다 이것을 먼저 본다.
4154
4193
  */
4155
4194
  localParams = /* @__PURE__ */ new Map();
4156
- /** 관측 구동(P0) — 이벤트를 접는 투영기와 그 사실. tick 과 섞이지 않게 명시적으로 들고 있다. */
4195
+ /** 관측 구동(P0) — 이벤트를 계산하는 투영기와 그 사실. tick 과 섞이지 않게 명시적으로 들고 있다. */
4157
4196
  observer;
4158
4197
  /**
4159
4198
  * **관측 리듀서의 재개점을 꺼낸다** — 호스트가 저장해 다음 기동에서 되돌릴 수 있게.
@@ -4426,7 +4465,7 @@ var FlowEngine = class {
4426
4465
  * 미러의 진실은 원천이다. 그래서 재고·위치·설비 상태는 스냅샷에서 심지 않는다 — 다음 계측·다음
4427
4466
  * 마스터 재읽기가 곧 정정하므로, 심으면 **떠난 물건이 되살아나는** 위험만 남는다.
4428
4467
  *
4429
- * 그런데 원천이 **애초에 다시 말해 주지 않는 축**이 있다. 우리가 접어 만든 것들이다:
4468
+ * 그런데 원천이 **애초에 다시 말해 주지 않는 축**이 있다. 우리가 계산해 만든 것들이다:
4430
4469
  * · 확인(ack)·조건이 처음 성립한 시각 — 사람이 한 일과 우리가 기억한 시각
4431
4470
  * · 열린 구간의 누적·적산 기준점·관측 이후 최대 — SCADA 는 「이번 15분에 지금까지 얼마」를 모른다
4432
4471
  *
@@ -4524,6 +4563,20 @@ var FlowEngine = class {
4524
4563
  ...m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {},
4525
4564
  /* 결과도 이어받는다 — 잃으면 예측이 **자격 만료를 모르는 현장**에서 출발한다(낙관 쪽으로 치우친다). */
4526
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),
4527
4580
  ...effectiveOnly(m)
4528
4581
  });
4529
4582
  }
@@ -4772,7 +4825,7 @@ var FlowEngine = class {
4772
4825
  */
4773
4826
  _acked = /* @__PURE__ */ new Set();
4774
4827
  /**
4775
- * 완료 시점에 **주체가 사라져 접은 작업 수** — 조용한 누락이 되지 않게 센다.
4828
+ * 완료 시점에 **주체가 사라져 제외한 작업 수** — 조용한 누락이 되지 않게 센다.
4776
4829
  *
4777
4830
  * 물품이 포장·출하·소비로 없어지는 것은 정상이지만, 그 물품을 향한 작업이 남아 있었다는 것은
4778
4831
  * 상류에 어긋남이 있다는 신호다. 0 이 아니면 그 사실을 소비처가 볼 수 있어야 한다.
@@ -5314,7 +5367,7 @@ var FlowEngine = class {
5314
5367
  * 어긋난다(2026-08-01 하루에 아홉 곳). 근본 해법은 **한 상태 모델 두 구동**이고, 이것은 그 실현
5315
5368
  * 가능성을 재는 스파이크다(design/plans/kernel-unification-live-observe.md P0).
5316
5369
  *
5317
- * 여기서는 **이미 검증된 조각을 조립**한다: 투영기가 이벤트를 접고, 그 결과를 씨앗 경로
5370
+ * 여기서는 **이미 검증된 조각을 조립**한다: 투영기가 이벤트를 계산하고, 그 결과를 씨앗 경로
5318
5371
  * (`hydrateObserved`)로 커널 상태에 주입한다. 그래서 관측으로 실행한 커널을 그대로 `fork`·`tick` 할 수
5319
5372
  * 있다 — "미러에서 예측한다" 가 별도 배관 없이 성립하는지가 이 스파이크의 질문이다.
5320
5373
  *
@@ -6287,7 +6340,7 @@ var FlowEngine = class {
6287
6340
  *
6288
6341
  * 그때 도메인 훅은 없는 것을 딛으려다 오류를 낸다(`order.gtin` · `item.location`). 그 예외 하나가
6289
6342
  * **예측 전체를 죽였다** — 사용자에게는 기능이 통째로 사라진 것으로 보였다. 그래서 완료 **전에**
6290
- * 여기서 묻고, 없으면 그 작업만 접는다.
6343
+ * 여기서 묻고, 없으면 그 작업만 제외한다.
6291
6344
  */
6292
6345
  missingContextOf(t) {
6293
6346
  if (t.itemEpc && !this.itemByRef(t.itemEpc)) return "item";
@@ -7736,7 +7789,7 @@ var MesKernel = class extends FlowEngine {
7736
7789
  *
7737
7790
  * 씨앗이 이미 이행된 오더를 주입하지 않으므로(남은 수량 0) 그 오더에 딸린 작업만 남을 수 있고,
7738
7791
  * 관측이 오더 연결을 담지 못한 작업도 있다. 그때 예전에는 `order.gtin` 에서 오류가 났고 **그 예외
7739
- * 하나가 정확도 추세 전체를 죽였다.** 이제 코어가 이 답을 보고 그 작업만 접는다.
7792
+ * 하나가 정확도 추세 전체를 죽였다.** 이제 코어가 이 답을 보고 그 작업만 제외한다.
7740
7793
  */
7741
7794
  canComplete(t) {
7742
7795
  return !!(t.orderId && this.orders.get(t.orderId));
@@ -8253,7 +8306,7 @@ var EmsKernel = class extends FlowEngine {
8253
8306
  * **뿌리 계량기** — 조상 중에 계량된 자리가 없는 계량 지점들.
8254
8307
  *
8255
8308
  * 계층 계량(수전 ⊃ 분기)에서 전부 더하면 이중 계상이 된다. 뿌리만 더하면 현장의 실제 부하가 된다.
8256
- * 모델이 그 계량기의 자리를 모르면 뿌리로 본다(모르는 것을 빼면 부하가 조용히 작아진다 — 그것이
8309
+ * 모델이 그 계량기의 자리를 모르면 뿌리로 본다(모르는 것을 빼면 부하가 오류 없이 작아진다 — 그것이
8257
8310
  * 「계약 안쪽」이라는 더 위험한 거짓을 만든다).
8258
8311
  */
8259
8312
  rootMeterIds() {
@@ -8326,7 +8379,7 @@ var EmsKernel = class extends FlowEngine {
8326
8379
  * "지금" 이 시뮬 기준시각(2026-01-01)에 얼어, 구간 마감·피크·감축 제안이 전부 8개월 전으로 기록됐다.
8327
8380
  *
8328
8381
  * 계측 자체는 원천 시각을 달고 있어서 화면의 순간값은 정상으로 보였다 — 그래서 조용한 결함이었다.
8329
- * 저널을 기간으로 접는 쪽(성과·요금·피더 배분)은 그 트윈에서 영원히 아무것도 찾지 못했다.
8382
+ * 저널을 기간으로 계산하는 쪽(성과·요금·피더 배분)은 그 트윈에서 영원히 아무것도 찾지 못했다.
8330
8383
  */
8331
8384
  apply(envelope) {
8332
8385
  if (envelope.eventType === ENERGY_EVENT.measured) {
@@ -8337,6 +8390,10 @@ var EmsKernel = class extends FlowEngine {
8337
8390
  this.applyEquipmentEnergy(envelope);
8338
8391
  return;
8339
8392
  }
8393
+ if (envelope.eventType === ENERGY_EVENT.generated) {
8394
+ this.applyGenerated(envelope);
8395
+ return;
8396
+ }
8340
8397
  super.apply(envelope);
8341
8398
  }
8342
8399
  /**
@@ -8351,6 +8408,51 @@ var EmsKernel = class extends FlowEngine {
8351
8408
  *
8352
8409
  * 온 값만 덮는다: 보내지 않은 필드는 그대로 둔다(없는 값을 0 으로 만들지 않는다).
8353
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();
8354
8456
  applyEquipmentEnergy(envelope) {
8355
8457
  const d = envelope.data;
8356
8458
  const id = String(d?.equipmentId ?? "").trim();
@@ -8390,6 +8492,9 @@ var EmsKernel = class extends FlowEngine {
8390
8492
  const kW = Number.isFinite(Number(d?.kW)) ? Number(d.kW) : void 0;
8391
8493
  const point = this.points.get(id) ?? { id, samplesInWindow: 0 };
8392
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;
8393
8498
  if (point.atMs === void 0 || atMs >= point.atMs) {
8394
8499
  point.atMs = atMs;
8395
8500
  if (kW !== void 0) point.kW = kW;
@@ -8404,12 +8509,18 @@ var EmsKernel = class extends FlowEngine {
8404
8509
  const roots = this.rootMeterIds();
8405
8510
  let total = 0;
8406
8511
  let declared = 0;
8512
+ let exported = 0;
8407
8513
  for (const p of this.points.values()) {
8408
8514
  if (!roots.has(p.id)) continue;
8409
8515
  const v = p.kW ?? 0;
8516
+ if (p.direction === "export") {
8517
+ exported += v;
8518
+ continue;
8519
+ }
8410
8520
  total += v;
8411
8521
  if (this.equipment.has(p.id)) declared += v;
8412
8522
  }
8523
+ if (exported > 0 && (w.exportKW === void 0 || exported > w.exportKW)) w.exportKW = exported;
8413
8524
  if (w.maxKW === void 0 || total > w.maxKW) {
8414
8525
  w.maxKW = total;
8415
8526
  w.maxDeclaredKW = declared;
@@ -8476,6 +8587,7 @@ var EmsKernel = class extends FlowEngine {
8476
8587
  /* 총부하도 사실로 남긴다 — 이것이 없으면 저널을 읽는 쪽은 「무엇이 깎였나」를 되짚을 수 없다
8477
8588
  (요금은 순수요로 매겨지지만, 그 수요를 만든 것은 설비 부하다). */
8478
8589
  ...w.grossMaxKW !== void 0 ? { grossMaxKW: w.grossMaxKW } : {},
8590
+ ...w.exportKW !== void 0 ? { exportKW: w.exportKW } : {},
8479
8591
  ...w.meanKW !== void 0 ? { meanKW: w.meanKW } : {},
8480
8592
  samples: w.samples,
8481
8593
  ...w.contractKW !== void 0 ? { contractKW: w.contractKW } : {},
@@ -8492,7 +8604,7 @@ var EmsKernel = class extends FlowEngine {
8492
8604
  * ── 지점별 몫을 함께 낸다 (2026-08-18) ────────────────────────────────
8493
8605
  *
8494
8606
  * 분기마다 계량기가 있는 현장에서 분기별 전력량은 **측정**이다. 그런데 그 사실이 저널에
8495
- * 없어서, 저널을 접는 쪽은 가동시간으로 **배분**할 수밖에 없었다 — 계량이 있는데 배분으로
8607
+ * 없어서, 저널을 계산하는 쪽은 가동시간으로 **배분**할 수밖에 없었다 — 계량이 있는데 배분으로
8496
8608
  * 말하는 것은 근거를 실제보다 약하게 보고하는 것이고, 그 위에 세운 SEU 판단이 무거운 값을 놓친다.
8497
8609
  *
8498
8610
  * 표본을 그대로 저널에 흘리는 대신 **구간마다 한 줄**로 낸다: 계측은 초당 여러 건이라 표본을
@@ -8828,6 +8940,23 @@ var EmsKernel = class extends FlowEngine {
8828
8940
  return void 0;
8829
8941
  }
8830
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
+ }
8831
8960
  stringProperty(resource, id) {
8832
8961
  for (const p of resource?.properties ?? []) {
8833
8962
  if (p?.id !== id) continue;
@@ -8846,10 +8975,10 @@ var EmsKernel = class extends FlowEngine {
8846
8975
  * 에너지의 연속성 — **SCADA 는 「이번 15분에 지금까지 얼마」를 모른다.**
8847
8976
  *
8848
8977
  * 계량기는 순간값과 적산값을 준다. 「이 구간의 최대」·「구간이 열릴 때의 적산」·「관측 이후 최대」는
8849
- * 우리가 접어 만든 값이라, 재기동하면 원천이 되풀어 주지 않는다. 잃으면 오류 없이 값이 작아진다 —
8978
+ * 우리가 계산해 만든 값이라, 재기동하면 원천이 되풀어 주지 않는다. 잃으면 오류 없이 값이 작아진다 —
8850
8979
  * 구간 전력량이 기준점을 새로 잡아 짧아지고, 그 구간의 피크가 부팅 이후로만 잡힌다(요금이 걸린 수다).
8851
8980
  *
8852
- * 지난 마감 구간도 이어받는다: 이미 저널에 있는 사실이지만, 화면이 그것을 보려고 매번 저널을 접지
8981
+ * 지난 마감 구간도 이어받는다: 이미 저널에 있는 사실이지만, 화면이 그것을 보려고 매번 저널을 계산하지
8853
8982
  * 않게 한다(자른 사실은 `closedTotal` 이 그대로 나른다).
8854
8983
  *
8855
8984
  * 지금 열린 구간이 **이미 지나간 것**이어도 그대로 받는다 — 다음 표본이 오면 커널이 그 구간을 정상
@@ -9027,6 +9156,44 @@ function ingestEnergyEquipmentRecords(records, opts) {
9027
9156
  }
9028
9157
  return { accepted, rejected };
9029
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
+ }
9030
9197
 
9031
9198
  // src/operational-ingest.ts
9032
9199
  var TASK_STATUS = ["created", "assigned", "in-progress", "completed"];
@@ -9037,7 +9204,7 @@ var SPECS = {
9037
9204
  task: {
9038
9205
  eventType: OP_EVENT.task,
9039
9206
  identity: "taskId",
9040
- /* 종류가 없으면 성과를 종류별로 접을 수 없고(선언된 시간·수율이 종류로 붙는다) 지어낼 수도 없다. */
9207
+ /* 종류가 없으면 성과를 종류별로 모을 수 없고(선언된 시간·수율이 종류로 붙는다) 지어낼 수도 없다. */
9041
9208
  required: ["taskId", "kind", "status"],
9042
9209
  fields: {
9043
9210
  taskId: "string",
@@ -9290,7 +9457,7 @@ function ingestOperationalRecords(records, opts) {
9290
9457
  }
9291
9458
  const allowed = spec.enums?.[name];
9292
9459
  if (allowed && !allowed.includes(v)) {
9293
- errors.push(`${kind}.${name} \uC774 \uCEE4\uB110\uC774 \uC811\uB294 \uB0B1\uB9D0\uC774 \uC544\uB2C8\uB2E4: ${JSON.stringify(v)} \u2014 \uBC1B\uB294 \uAC12\uC740 ${allowed.join(" \xB7 ")}`);
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 ")}`);
9294
9461
  break;
9295
9462
  }
9296
9463
  data[name] = v;
@@ -9629,6 +9796,7 @@ function retiredVocabularyIn(line) {
9629
9796
  MES_BIZSTEP,
9630
9797
  MES_LOCATION_TYPES,
9631
9798
  MES_TYPES,
9799
+ METER_DIRECTION,
9632
9800
  MesKernel,
9633
9801
  OPERATION_PROPERTY,
9634
9802
  OP_EVENT,
@@ -9697,11 +9865,13 @@ function retiredVocabularyIn(line) {
9697
9865
  inWorkCalendarAt,
9698
9866
  ingest,
9699
9867
  ingestEnergyEquipmentRecords,
9868
+ ingestEnergyGenerationRecords,
9700
9869
  ingestEnergyRecords,
9701
9870
  ingestOperationalRecords,
9702
9871
  isAggregationRecord,
9703
9872
  isElectricalLocationType,
9704
9873
  isEnergyEquipmentRecord,
9874
+ isEnergyGenerationRecord,
9705
9875
  isEnergyRecord,
9706
9876
  isEquipmentLevel,
9707
9877
  isOperationalRecord,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/twin-kernel",
3
- "version": "0.7.65",
3
+ "version": "0.7.66",
4
4
  "type": "module",
5
5
  "description": "Twin Domain Kernel — framework-agnostic, zero-dep (domain + sim + 3-channel contract). WMS/YMS/MES, EPCIS 2.0 · ISA-95.",
6
6
  "publishConfig": {