@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.
@@ -99,8 +99,8 @@ export function isEnergyEquipmentRecord(record) {
99
99
  return typeof r.equipmentId === 'string' && r.equipmentId.trim().length > 0 && r.epc === undefined && r.meterId === undefined;
100
100
  }
101
101
  /**
102
- * 설비 에너지 레코드 → 봉투. 계량과 같은 규율이다: 지어낼 수 없는 것이 빠지면 거부하고, 거부한 것은
103
- * 이유와 함께 돌려준다.
102
+ * 설비 에너지 레코드 → 봉투. 계량과 같은 규율이다: 지어낼 수 없는 것이 빠지면 받지 않고, 받지 않은 것은
103
+ * 이유와 함께 알린다.
104
104
  *
105
105
  * **값이 하나도 없는 레코드는 거부한다** — 계량과 다른 점이다. 계량은 「응답했다」는 사실 자체가
106
106
  * 관측이지만(그래서 kW 없는 표본을 받는다), 여기서는 바꿀 상태가 없는 것을 사실로 적을 이유가 없다.
@@ -192,3 +192,59 @@ export function ingestEnergyEquipmentRecords(records, opts) {
192
192
  }
193
193
  return { accepted, rejected };
194
194
  }
195
+ /** 이 레코드가 발전 적산인가 — 어느 갈래로 보낼지를 한 곳에서 정한다. */
196
+ export function isEnergyGenerationRecord(record) {
197
+ if (!record || typeof record !== 'object')
198
+ return false;
199
+ const r = record;
200
+ /* 설비를 말하고 적산을 실었으면 발전이다. 계량 어휘가 함께 있으면 어느 쪽인지 알 수 없어 받지 않는다. */
201
+ return (typeof r.equipmentId === 'string' &&
202
+ r.equipmentId.trim().length > 0 &&
203
+ r.kWh !== undefined &&
204
+ r.meterId === undefined &&
205
+ r.epc === undefined);
206
+ }
207
+ /**
208
+ * 발전 적산 레코드 → 봉투. 계량과 같은 규율이다: 지어낼 수 없는 것이 빠지면 받지 않고, 받지 않은 것은
209
+ * 이유와 함께 알린다.
210
+ *
211
+ * **적산이 없는 레코드는 받지 않는다** — 이 문의 값은 그것 하나다. 순시 출력은 설비 상태 문이 받는다
212
+ * (`EnergyEquipmentRecord.generatedKW`).
213
+ */
214
+ export function ingestEnergyGenerationRecords(records, opts) {
215
+ const list = records === undefined || records === null ? [] : Array.isArray(records) ? records : [records];
216
+ const accepted = [];
217
+ const rejected = [];
218
+ let seq = 0;
219
+ for (const r of list) {
220
+ const errors = [];
221
+ const equipmentId = String(r?.equipmentId ?? '').trim();
222
+ if (!equipmentId)
223
+ errors.push('equipmentId 가 없다 — 어느 설비가 만든 것인지 지어낼 수 없다');
224
+ const raw = r?.kWh;
225
+ const kWh = Number(raw);
226
+ if (raw === undefined || raw === null || !Number.isFinite(kWh)) {
227
+ errors.push('kWh 가 없다 — 이 문이 받는 값은 적산 하나다(순시 출력은 설비 상태로 보낸다)');
228
+ }
229
+ else if (kWh < 0) {
230
+ /* 음수 적산은 계기가 낼 수 있는 값이 아니다. 받아 두면 그 뒤의 차분이 전부 거짓이 된다. */
231
+ errors.push(`kWh 가 음수다(${kWh}) — 적산은 뒤로 가더라도 음수가 되지 않는다`);
232
+ }
233
+ const eventTime = String(r?.at ?? '').trim() || opts.defaultEventTime || '';
234
+ if (!eventTime)
235
+ errors.push('at 이 없고 기본 시각도 주지 않았다 — 언제 잰 것인지 지어낼 수 없다');
236
+ if (errors.length) {
237
+ rejected.push({ record: r, errors });
238
+ continue;
239
+ }
240
+ const data = { equipmentId, kWh, at: eventTime };
241
+ accepted.push({
242
+ eventId: `${opts.tenantId}-generated-${++seq}`,
243
+ eventType: ENERGY_EVENT.generated,
244
+ eventTime,
245
+ tenantId: opts.tenantId,
246
+ data
247
+ });
248
+ }
249
+ return { accepted, rejected };
250
+ }
package/dist/epcis.d.ts CHANGED
@@ -11,12 +11,12 @@ export declare const DISP: {
11
11
  /**
12
12
  * **기한이 지났다** — CBV `expired`.
13
13
  *
14
- * ── 왜 `non_sellable` 로 접지 않나 (2026-08-24) ─────────────────────────────
14
+ * ── 왜 `non_sellable` 로 계산하지 않나 (2026-08-24) ─────────────────────────────
15
15
  * 커널의 `non_sellable` 은 CBV 의 `non_sellable_other`, 즉 **「그 밖의 이유」**다. 기한 지남을 거기
16
16
  * 넣으면 「기한이 지나 못 판다」와 「깨져서 못 판다」가 같은 값이 되고, 화면은 회수·폐기의 사유를
17
17
  * 구별할 수 없다. 식품에서 그 둘은 다른 조치다.
18
18
  *
19
- * 그리고 표준에 **정확한 낱말이 있다** — 접는 것은 있는 낱말을 버리는 것이다.
19
+ * 그리고 표준에 **정확한 낱말이 있다** — 합치는 것은 있는 낱말을 버리는 것이다.
20
20
  *
21
21
  * ── 기한 날짜와 다른 축이다 ────────────────────────────────────────────────
22
22
  * `ItemState.expiry` 는 **날짜**이고 이것은 **상태**다. 날짜가 있으면 「지났나」는 파생이지만, 원본이
package/dist/epcis.js CHANGED
@@ -20,12 +20,12 @@ export const DISP = {
20
20
  /**
21
21
  * **기한이 지났다** — CBV `expired`.
22
22
  *
23
- * ── 왜 `non_sellable` 로 접지 않나 (2026-08-24) ─────────────────────────────
23
+ * ── 왜 `non_sellable` 로 계산하지 않나 (2026-08-24) ─────────────────────────────
24
24
  * 커널의 `non_sellable` 은 CBV 의 `non_sellable_other`, 즉 **「그 밖의 이유」**다. 기한 지남을 거기
25
25
  * 넣으면 「기한이 지나 못 판다」와 「깨져서 못 판다」가 같은 값이 되고, 화면은 회수·폐기의 사유를
26
26
  * 구별할 수 없다. 식품에서 그 둘은 다른 조치다.
27
27
  *
28
- * 그리고 표준에 **정확한 낱말이 있다** — 접는 것은 있는 낱말을 버리는 것이다.
28
+ * 그리고 표준에 **정확한 낱말이 있다** — 합치는 것은 있는 낱말을 버리는 것이다.
29
29
  *
30
30
  * ── 기한 날짜와 다른 축이다 ────────────────────────────────────────────────
31
31
  * `ItemState.expiry` 는 **날짜**이고 이것은 **상태**다. 날짜가 있으면 「지났나」는 파생이지만, 원본이
@@ -16,14 +16,14 @@ export declare class EventJournal {
16
16
  /** 이벤트열 → 상태 재구성(시간여행). `model` = 그 시점의 트윈 모델(토폴로지·자원). */
17
17
  export declare function replay(model: TwinModelDef, events: readonly CanonicalEnvelope[]): ProjectedState;
18
18
  /**
19
- * **재개점에서 이어 접는다** — 0부터 다시 접지 않는다.
19
+ * **재개점에서 이어서 계산한다** — 0부터 다시 계산하지 않는다.
20
20
  *
21
21
  * ── 왜 (2026-08-18 실측) ────────────────────────────────────────────────────
22
- * 저널이 27만 건인 트윈에서 과거 상태를 물으면 전부 다시 접어야 했다. 그런데 우리는 이미 주기적으로
23
- * 재개점을 남기고 있다 — 그 지점부터 **뒤에 일어난 것만** 접으면 같은 답이 나온다.
22
+ * 저널이 27만 건인 트윈에서 과거 상태를 물으면 전부 다시 계산해야 했다. 그런데 우리는 이미 주기적으로
23
+ * 재개점을 남기고 있다 — 그 지점부터 **뒤에 일어난 것만** 계산하면 같은 답이 나온다.
24
24
  *
25
25
  * 「같은 답」은 말로 보장되지 않는다: 재개점이 리듀서의 **내부 상태 전부**여야 하고(보기가 아니라),
26
- * 그 사실은 시험이 증명한다(0부터 접기 == 재개점 + 꼬리). 그래서 이 함수는 새 재개점도 함께 낸다 —
26
+ * 그 사실은 시험이 증명한다(0부터 계산하기 == 재개점 + 꼬리). 그래서 이 함수는 새 재개점도 함께 낸다 —
27
27
  * 소비처가 그것을 저장해 다음 꼬리를 또 이어 붙일 수 있게.
28
28
  *
29
29
  * 구조가 바뀐 구간은 여기서 다루지 않는다(`replaySegments` 의 몫이다) — 재개점은 **한 구조 안에서**
@@ -33,7 +33,7 @@ export declare function replayFrom(model: TwinModelDef, checkpoint: ReducerCheck
33
33
  state: ProjectedState;
34
34
  checkpoint: ReducerCheckpoint;
35
35
  };
36
- /** 이벤트열을 접고 **재개점도 함께** 낸다 — 다음 번에 이어 붙일 수 있게. */
36
+ /** 이벤트열을 계산하고 **재개점도 함께** 낸다 — 다음 번에 이어 붙일 수 있게. */
37
37
  export declare function replayWithCheckpoint(model: TwinModelDef, events: readonly CanonicalEnvelope[]): {
38
38
  state: ProjectedState;
39
39
  checkpoint: ReducerCheckpoint;
@@ -65,10 +65,10 @@ export interface SegmentShift extends StructureShift {
65
65
  * **구조가 바뀐 이력까지 이어서 재생한다.**
66
66
  *
67
67
  * 공장은 바뀐다. 지금까지는 구조가 바뀌면 저널을 지우는 것이 유일한 길이었다 — 안 지우면 옛 이벤트를
68
- * 새 공장에 대고 접게 되어 이력이 거짓말을 하기 때문이다(도장 부스가 둘이던 시절의 사실을 여섯 개짜리
69
- * 공장에 접는다). 역사를 잃거나 거짓말을 하거나, 둘뿐이었다.
68
+ * 새 공장에 대고 계산하게 되어 이력이 거짓말을 하기 때문이다(도장 부스가 둘이던 시절의 사실을 여섯 개짜리
69
+ * 공장에 계산한다). 역사를 잃거나 거짓말을 하거나, 둘뿐이었다.
70
70
  *
71
- * 셋째 길이 이것이다: 마디마다 **그때의 구조**로 접고, 경계에서 구조만 전환한다(관측된 사실은 이어
71
+ * 셋째 길이 이것이다: 마디마다 **그때의 구조**로 계산하고, 경계에서 구조만 전환한다(관측된 사실은 이어
72
72
  * 간다). 그러면 이력이 "그때 그 공장의 사실" 로 계속 읽힌다.
73
73
  *
74
74
  * 경계에서 사라진 자원은 결과에 실어 보낸다 — 수가 줄어든 것을 사용자가 눈치채지 못하면 안 된다.
@@ -40,14 +40,14 @@ export function replay(model, events) {
40
40
  return proj.snapshot();
41
41
  }
42
42
  /**
43
- * **재개점에서 이어 접는다** — 0부터 다시 접지 않는다.
43
+ * **재개점에서 이어서 계산한다** — 0부터 다시 계산하지 않는다.
44
44
  *
45
45
  * ── 왜 (2026-08-18 실측) ────────────────────────────────────────────────────
46
- * 저널이 27만 건인 트윈에서 과거 상태를 물으면 전부 다시 접어야 했다. 그런데 우리는 이미 주기적으로
47
- * 재개점을 남기고 있다 — 그 지점부터 **뒤에 일어난 것만** 접으면 같은 답이 나온다.
46
+ * 저널이 27만 건인 트윈에서 과거 상태를 물으면 전부 다시 계산해야 했다. 그런데 우리는 이미 주기적으로
47
+ * 재개점을 남기고 있다 — 그 지점부터 **뒤에 일어난 것만** 계산하면 같은 답이 나온다.
48
48
  *
49
49
  * 「같은 답」은 말로 보장되지 않는다: 재개점이 리듀서의 **내부 상태 전부**여야 하고(보기가 아니라),
50
- * 그 사실은 시험이 증명한다(0부터 접기 == 재개점 + 꼬리). 그래서 이 함수는 새 재개점도 함께 낸다 —
50
+ * 그 사실은 시험이 증명한다(0부터 계산하기 == 재개점 + 꼬리). 그래서 이 함수는 새 재개점도 함께 낸다 —
51
51
  * 소비처가 그것을 저장해 다음 꼬리를 또 이어 붙일 수 있게.
52
52
  *
53
53
  * 구조가 바뀐 구간은 여기서 다루지 않는다(`replaySegments` 의 몫이다) — 재개점은 **한 구조 안에서**
@@ -60,7 +60,7 @@ export function replayFrom(model, checkpoint, events) {
60
60
  proj.apply(e);
61
61
  return { state: proj.snapshot(), checkpoint: proj.serialize() };
62
62
  }
63
- /** 이벤트열을 접고 **재개점도 함께** 낸다 — 다음 번에 이어 붙일 수 있게. */
63
+ /** 이벤트열을 계산하고 **재개점도 함께** 낸다 — 다음 번에 이어 붙일 수 있게. */
64
64
  export function replayWithCheckpoint(model, events) {
65
65
  const proj = new StateProjector(model);
66
66
  for (const e of events)
@@ -71,10 +71,10 @@ export function replayWithCheckpoint(model, events) {
71
71
  * **구조가 바뀐 이력까지 이어서 재생한다.**
72
72
  *
73
73
  * 공장은 바뀐다. 지금까지는 구조가 바뀌면 저널을 지우는 것이 유일한 길이었다 — 안 지우면 옛 이벤트를
74
- * 새 공장에 대고 접게 되어 이력이 거짓말을 하기 때문이다(도장 부스가 둘이던 시절의 사실을 여섯 개짜리
75
- * 공장에 접는다). 역사를 잃거나 거짓말을 하거나, 둘뿐이었다.
74
+ * 새 공장에 대고 계산하게 되어 이력이 거짓말을 하기 때문이다(도장 부스가 둘이던 시절의 사실을 여섯 개짜리
75
+ * 공장에 계산한다). 역사를 잃거나 거짓말을 하거나, 둘뿐이었다.
76
76
  *
77
- * 셋째 길이 이것이다: 마디마다 **그때의 구조**로 접고, 경계에서 구조만 전환한다(관측된 사실은 이어
77
+ * 셋째 길이 이것이다: 마디마다 **그때의 구조**로 계산하고, 경계에서 구조만 전환한다(관측된 사실은 이어
78
78
  * 간다). 그러면 이력이 "그때 그 공장의 사실" 로 계속 읽힌다.
79
79
  *
80
80
  * 경계에서 사라진 자원은 결과에 실어 보낸다 — 수가 줄어든 것을 사용자가 눈치채지 못하면 안 된다.
@@ -2,6 +2,34 @@ import type { CanonicalEnvelope } from './contract.ts';
2
2
  import type { EpcisEvent } from './epcis.ts';
3
3
  /** 매핑 값: 리터럴, 또는 소스 필드 참조("$.field.path"). */
4
4
  export type MapValue = string;
5
+ /**
6
+ * **정정 선언 매핑** — 「이 사건은 없던 일이 되었다」를 표준 어휘로 옮기는 자리.
7
+ *
8
+ * ── 왜 뒤늦게 생겼나 (2026-08-25) ───────────────────────────────────────────
9
+ * `quantityList` · `ilmd` 와 **똑같은 모양의 결함**이었다. 받는 쪽은 처음부터 다 있었다 —
10
+ * 타입(`ErrorDeclaration`) · 검증(선언 시각·정정 대상 검사) · 리듀서(정정 목록에 적고 상태는
11
+ * 흔들지 않는다). 옮길 자리만 없었다.
12
+ *
13
+ * 실 시스템은 잘못 보낸 것을 정정한다. 그 정정을 실을 자리가 없으면 두 길뿐인데 둘 다 나쁘다:
14
+ * 취소를 **새 사실**로 보내 재고를 두 번 흔들거나, 아예 보내지 않아 지난 기록이 일어나지 않은 일을
15
+ * 일어난 것으로 계속 말하게 한다. 표준이 세 번째 길을 정해 두었다 — 앞선 사건을 지우지 않고
16
+ * 「정정되었다」를 함께 남긴다(EPCIS 2.0 §7.5).
17
+ *
18
+ * **세 값은 함께여야 뜻이 있다.** 그래서 평평한 세 필드가 아니라 묶음 하나로 둔다 — 반쯤 채운
19
+ * 정정 선언은 정정이 아니다.
20
+ *
21
+ * 이유(`reason`)는 CBV 어휘다(`did_not_occur` · `incorrect_data`). 연결된 시스템의 낱말
22
+ * (`UNDO_UNLOADING` 같은 것)을 그 둘 중 하나로 옮기는 것은 커넥터의 일이다 — 여기서 그 번역을
23
+ * 대신하면 시스템마다 다른 규칙이 이 파일에 쌓인다.
24
+ */
25
+ export interface ErrorDeclarationMapping {
26
+ /** 정정을 **선언한** 시각. 사건이 일어난 시각이 아니다 — 없으면 지어내지 않고 이유와 함께 알린다. */
27
+ declarationTime: MapValue;
28
+ /** CBV 이유 어휘. 연결된 시스템이 이유를 말하지 않으면 비워 둔다(둘 중 하나로 짐작하지 않는다). */
29
+ reason?: MapValue;
30
+ /** 정정이 가리키는 원래 사건들의 `eventID`("$.correctiveEventIDs") — 문자열 배열. */
31
+ correctiveEventIDs?: MapValue;
32
+ }
5
33
  /** ObjectEvent 매핑 스펙(선언적). */
6
34
  export interface ObjectEventMapping {
7
35
  type: 'ObjectEvent';
@@ -47,6 +75,8 @@ export interface ObjectEventMapping {
47
75
  ilmd?: MapValue;
48
76
  readPoint?: MapValue;
49
77
  bizLocation?: MapValue;
78
+ /** 이 레코드가 앞선 사건의 정정이면 그 선언(§ErrorDeclarationMapping). */
79
+ errorDeclaration?: ErrorDeclarationMapping;
50
80
  }
51
81
  /**
52
82
  * AggregationEvent 매핑 스펙 — **담김의 사실**(무엇이 무엇에 실렸나).
@@ -74,6 +104,8 @@ export interface AggregationEventMapping {
74
104
  childQuantityList?: MapValue;
75
105
  readPoint?: MapValue;
76
106
  bizLocation?: MapValue;
107
+ /** 이 레코드가 앞선 사건의 정정이면 그 선언(§ErrorDeclarationMapping). */
108
+ errorDeclaration?: ErrorDeclarationMapping;
77
109
  }
78
110
  /**
79
111
  * **변환의 사실** — 무엇이 들어가 무엇이 나왔나(EPCIS `TransformationEvent`).
@@ -104,6 +136,8 @@ export interface TransformationEventMapping {
104
136
  transformationID?: MapValue;
105
137
  readPoint?: MapValue;
106
138
  bizLocation?: MapValue;
139
+ /** 이 레코드가 앞선 사건의 정정이면 그 선언(§ErrorDeclarationMapping). */
140
+ errorDeclaration?: ErrorDeclarationMapping;
107
141
  }
108
142
  export type EventMapping = ObjectEventMapping | AggregationEventMapping | TransformationEventMapping;
109
143
  /**
@@ -143,7 +177,6 @@ export interface IngestResult {
143
177
  }[];
144
178
  }
145
179
  type Rec = Record<string, unknown>;
146
- /** 단일 레코드 → 정규 EPCIS 이벤트 + 매핑에서 드러난 문제(검증은 `ingest` 가 이어서 한다). */
147
180
  export declare function mapRecordChecked(record: Rec, mapping: EventMapping, eventTime: string): {
148
181
  event: EpcisEvent;
149
182
  errors: string[];
@@ -120,12 +120,49 @@ function resolveObject(v, record, field, errors) {
120
120
  return Object.keys(o).length ? o : undefined;
121
121
  }
122
122
  /** 단일 레코드 → 정규 EPCIS 이벤트 + 매핑에서 드러난 문제(검증은 `ingest` 가 이어서 한다). */
123
+ /**
124
+ * 정정 선언 묶음 → 표준 `ErrorDeclaration`.
125
+ *
126
+ * 선언 시각이 없으면 **이유와 함께 알리고 붙이지 않는다.** 사건 시각으로 메우고 싶어지는 자리인데,
127
+ * 그러면 「언제 정정했나」가 「언제 일어났나」와 같아져 둘을 다시 갈라낼 방법이 없다.
128
+ */
129
+ function resolveErrorDeclaration(spec, record, errors) {
130
+ if (!spec)
131
+ return undefined;
132
+ const declarationTime = resolve(spec.declarationTime, record, 'errorDeclaration.declarationTime', errors);
133
+ const reason = resolve(spec.reason, record, 'errorDeclaration.reason', errors);
134
+ const correctiveEventIDs = resolveList(spec.correctiveEventIDs, record, 'errorDeclaration.correctiveEventIDs', errors);
135
+ /*
136
+ * ★ **자리가 있다는 것과 정정이라는 것은 다르다.**
137
+ *
138
+ * 매핑은 한 번 적고 모든 레코드가 그것을 지난다. 정정은 드문 사건이므로 **거의 모든 레코드에는 이
139
+ * 세 값이 없다.** 그때 오류를 내면 평범한 관측 전부가 매핑 오류를 달게 되고, 라이브가 통째로
140
+ * 막힌다 — 자리를 낸 것이 곧 통로를 막는 일이 된다.
141
+ *
142
+ * 그래서 아무 말도 없으면 정정이 아닌 것이다. **반쯤 말한 것**만 오류다: 이유나 정정 대상은 있는데
143
+ * 선언 시각이 없으면 정정하려던 것이 분명한데 언제인지 모르는 것이고, 그 시각은 지어낼 수 없다.
144
+ */
145
+ if (!declarationTime) {
146
+ if (reason || correctiveEventIDs.length) {
147
+ errors.push('errorDeclaration 이 반쯤 왔다 — 이유·정정 대상은 있는데 declarationTime 이 없다(정정 시각은 지어낼 수 없다)');
148
+ }
149
+ return undefined;
150
+ }
151
+ return {
152
+ declarationTime,
153
+ ...(reason ? { reason } : {}),
154
+ ...(correctiveEventIDs.length ? { correctiveEventIDs } : {})
155
+ };
156
+ }
123
157
  export function mapRecordChecked(record, mapping, eventTime) {
124
158
  const errors = [];
125
159
  const bizStep = resolve(mapping.bizStep, record, 'bizStep', errors) ?? '';
126
160
  const disposition = resolve(mapping.disposition, record, 'disposition', errors);
127
161
  const readPoint = resolve(mapping.readPoint, record, 'readPoint', errors);
128
162
  const bizLocation = resolve(mapping.bizLocation, record, 'bizLocation', errors);
163
+ /* 정정 선언은 표준 **헤더**의 것이므로 세 종류 모두에 담긴다(§7.5). 한 종류에만 내면 같은 결함이
164
+ 나머지 둘에 남는다 — 그것을 이미 두 번 겪었다(`quantityList` · `ilmd`). */
165
+ const errorDeclaration = resolveErrorDeclaration(mapping.errorDeclaration, record, errors);
129
166
  if (mapping.type === 'TransformationEvent') {
130
167
  const inputEPCList = resolveList(mapping.inputEPCList, record, 'inputEPCList', errors);
131
168
  const inputQuantityList = resolveQuantityList(mapping.inputQuantityList, record, 'inputQuantityList', errors);
@@ -156,6 +193,7 @@ export function mapRecordChecked(record, mapping, eventTime) {
156
193
  ...(outputEPCList.length ? { outputEPCList } : {}),
157
194
  ...(outputQuantityList.length ? { outputQuantityList } : {}),
158
195
  ...(transformationID ? { transformationID } : {}),
196
+ ...(errorDeclaration ? { errorDeclaration } : {}),
159
197
  readPoint, bizLocation
160
198
  }),
161
199
  errors
@@ -172,6 +210,7 @@ export function mapRecordChecked(record, mapping, eventTime) {
172
210
  /* 없는 쪽은 **필드를 만들지 않는다** — 빈 배열을 실으면 「자식이 없다」고 말하는 것이 된다. */
173
211
  ...(childEPCs.length ? { childEPCs } : {}),
174
212
  ...(childQuantityList.length ? { childQuantityList } : {}),
213
+ ...(errorDeclaration ? { errorDeclaration } : {}),
175
214
  readPoint, bizLocation
176
215
  }),
177
216
  errors
@@ -192,6 +231,7 @@ export function mapRecordChecked(record, mapping, eventTime) {
192
231
  epcList: epc ? [epc] : [],
193
232
  ...(quantityList.length ? { quantityList } : {}),
194
233
  ...(ilmd ? { ilmd } : {}),
234
+ ...(errorDeclaration ? { errorDeclaration } : {}),
195
235
  readPoint, bizLocation
196
236
  }),
197
237
  errors
@@ -365,6 +365,15 @@ export declare function deriveAttentions(view: {
365
365
  goodCount?: number;
366
366
  scrapCount?: number;
367
367
  offShift?: boolean;
368
+ /**
369
+ * 이 설비 소식이 끊긴 시간, 그리고 그때까지 가장 오래 끊겼던 시간(밀리초).
370
+ * 실제 현장을 따라가는 트윈만 이 값을 넣는다.
371
+ *
372
+ * 값이 없으면 이 항목은 판단하지 않는다. 스스로 상태를 만들어 가는 트윈에는 소식이 끊긴다는
373
+ * 것이 없으므로 값을 넣지 않는다(`tasks` · `now` 도 같은 방식이다).
374
+ */
375
+ silentMs?: number;
376
+ longestSilenceBeforeMs?: number;
368
377
  }[];
369
378
  locations: {
370
379
  id: string;
@@ -541,7 +550,7 @@ export declare abstract class FlowEngine implements TwinKernel {
541
550
  * 양품률 시임(선택) — **이력에서 배운 수율.** 소요시간과 같은 자리·같은 규율이다 (2026-08-19).
542
551
  *
543
552
  * 없으면 선언값(현장→원천), 그것도 없으면 도메인 상수. 무엇을 썼는지는 `specCoverage()` 가 밝힌다.
544
- * 주입은 호스트가 한다(저널을 접는 쪽이 이력을 안다) — 커널은 시임만 든다.
553
+ * 주입은 호스트가 한다(저널을 계산하는 쪽이 이력을 안다) — 커널은 시임만 든다.
545
554
  */
546
555
  yieldEstimator?: YieldEstimator;
547
556
  /**
@@ -563,7 +572,7 @@ export declare abstract class FlowEngine implements TwinKernel {
563
572
  * 읽는 쪽(`paramNumber`·`paramDuration`)이 명세 행보다 이것을 먼저 본다.
564
573
  */
565
574
  protected localParams: Map<string, Map<string, string>>;
566
- /** 관측 구동(P0) — 이벤트를 접는 투영기와 그 사실. tick 과 섞이지 않게 명시적으로 들고 있다. */
575
+ /** 관측 구동(P0) — 이벤트를 계산하는 투영기와 그 사실. tick 과 섞이지 않게 명시적으로 들고 있다. */
567
576
  private observer?;
568
577
  /**
569
578
  * **관측 리듀서의 재개점을 꺼낸다** — 호스트가 저장해 다음 기동에서 되돌릴 수 있게.
@@ -736,7 +745,7 @@ export declare abstract class FlowEngine implements TwinKernel {
736
745
  * 미러의 진실은 원천이다. 그래서 재고·위치·설비 상태는 스냅샷에서 심지 않는다 — 다음 계측·다음
737
746
  * 마스터 재읽기가 곧 정정하므로, 심으면 **떠난 물건이 되살아나는** 위험만 남는다.
738
747
  *
739
- * 그런데 원천이 **애초에 다시 말해 주지 않는 축**이 있다. 우리가 접어 만든 것들이다:
748
+ * 그런데 원천이 **애초에 다시 말해 주지 않는 축**이 있다. 우리가 계산해 만든 것들이다:
740
749
  * · 확인(ack)·조건이 처음 성립한 시각 — 사람이 한 일과 우리가 기억한 시각
741
750
  * · 열린 구간의 누적·적산 기준점·관측 이후 최대 — SCADA 는 「이번 15분에 지금까지 얼마」를 모른다
742
751
  *
@@ -791,7 +800,7 @@ export declare abstract class FlowEngine implements TwinKernel {
791
800
  * 미러는 그 작업을 굴리지 않는다 — **들은 것을 말할 뿐이다.** 그런데 규칙이 하나였으므로 관측된
792
801
  * 사실이 씨앗 규칙에 걸려 사라졌다.
793
802
  *
794
- * 실측: 포천 미러의 저널에 `task.status` 5,786건이 있고 투영기로 접으면 작업 2,881건인데
803
+ * 실측: 포천 미러의 저널에 `task.status` 5,786건이 있고 투영기로 계산하면 작업 2,881건인데
795
804
  * `getSnapshot().tasks` 는 **0** 이었다. 화면은 「아직 하나도 없습니다」라고 말했고 사용자가
796
805
  * 반나절을 찾았다. 재현하면 관측기 5건 → 커널 0건이고, 걸린 조건은 오더였다.
797
806
  *
@@ -826,7 +835,7 @@ export declare abstract class FlowEngine implements TwinKernel {
826
835
  */
827
836
  private _acked;
828
837
  /**
829
- * 완료 시점에 **주체가 사라져 접은 작업 수** — 조용한 누락이 되지 않게 센다.
838
+ * 완료 시점에 **주체가 사라져 제외한 작업 수** — 조용한 누락이 되지 않게 센다.
830
839
  *
831
840
  * 물품이 포장·출하·소비로 없어지는 것은 정상이지만, 그 물품을 향한 작업이 남아 있었다는 것은
832
841
  * 상류에 어긋남이 있다는 신호다. 0 이 아니면 그 사실을 소비처가 볼 수 있어야 한다.
@@ -946,7 +955,7 @@ export declare abstract class FlowEngine implements TwinKernel {
946
955
  * 어긋난다(2026-08-01 하루에 아홉 곳). 근본 해법은 **한 상태 모델 두 구동**이고, 이것은 그 실현
947
956
  * 가능성을 재는 스파이크다(design/plans/kernel-unification-live-observe.md P0).
948
957
  *
949
- * 여기서는 **이미 검증된 조각을 조립**한다: 투영기가 이벤트를 접고, 그 결과를 씨앗 경로
958
+ * 여기서는 **이미 검증된 조각을 조립**한다: 투영기가 이벤트를 계산하고, 그 결과를 씨앗 경로
950
959
  * (`hydrateObserved`)로 커널 상태에 주입한다. 그래서 관측으로 실행한 커널을 그대로 `fork`·`tick` 할 수
951
960
  * 있다 — "미러에서 예측한다" 가 별도 배관 없이 성립하는지가 이 스파이크의 질문이다.
952
961
  *
@@ -1425,7 +1434,7 @@ export declare abstract class FlowEngine implements TwinKernel {
1425
1434
  *
1426
1435
  * 그때 도메인 훅은 없는 것을 딛으려다 오류를 낸다(`order.gtin` · `item.location`). 그 예외 하나가
1427
1436
  * **예측 전체를 죽였다** — 사용자에게는 기능이 통째로 사라진 것으로 보였다. 그래서 완료 **전에**
1428
- * 여기서 묻고, 없으면 그 작업만 접는다.
1437
+ * 여기서 묻고, 없으면 그 작업만 제외한다.
1429
1438
  */
1430
1439
  protected missingContextOf(t: FlowTask): 'item' | 'order' | 'domain' | undefined;
1431
1440
  /**
@@ -16,6 +16,8 @@ import { OP_PARAM } from "./domain-definition.js";
16
16
  import { parseIsoDuration } from "./iso-duration.js";
17
17
  import { analyzeCapacity } from "./capacity.js";
18
18
  import { operationsCapabilityOf } from "./operations-capability.js";
19
+ /* 에너지 상태 필드는 **능력 선언에서 뽑는다** — 손으로 적으면 능력에 필드를 더한 날 스냅샷이 빠진다. */
20
+ import { stateFieldsOf } from "./capability.js";
19
21
  const BASE_EPOCH = Date.parse('2026-01-01T00:00:00Z');
20
22
  /**
21
23
  * 선언한 유효 기간만 뽑는다 — 마스터가 말하지 않으면 필드를 만들지 않는다.
@@ -106,6 +108,43 @@ thresholds) {
106
108
  suggestedAction: { code: 'act.repair', command: CMD.resourceRepair, args: { resourceId: m.id } }
107
109
  });
108
110
  }
111
+ /*
112
+ * 이 설비 소식이 지난 간격보다 오래 끊겼다는 신호. 설비가 멈췄다는 뜻이 아니고, 지금 상태를 우리가
113
+ * 모르고 있다는 뜻이다.
114
+ *
115
+ * ── 왜 필요한가 ───────────────────────────────────────────────────────────
116
+ * 연결된 시스템에서 값이 오다가 끊기는 일은 흔하다(통신 두절, 야간 정지, 커넥터 중단). 그때
117
+ * 트윈은 마지막으로 받은 값을 그대로 들고 있고, 화면은 그 값을 지금 상태로 그린다. 어제 오후에
118
+ * 받은 「가동 중」이 오늘 아침에도 「가동 중」으로 보인다. 오류가 나지 않으므로 아무도 확인하지
119
+ * 않는다. 값이 비어 있으면 사람이 의심하지만, 오래된 값이 채워져 있으면 의심하지 않는다.
120
+ *
121
+ * ── 왜 상태 필드가 아니라 신호인가 ────────────────────────────────────────
122
+ * 「가동 중」은 설비의 상태이고, 「그 값을 언제 받았나」는 우리 쪽 사정이다. 두 가지를 한 필드에
123
+ * 담으면 화면과 집계가 구별할 수 없다. 물품 쪽에서 같은 이유로 `seenAtMs` 를 상태에서 뺐다.
124
+ *
125
+ * ── 판단 기준을 우리가 정하지 않는다 ──────────────────────────────────────
126
+ * 「몇 분 넘게 값이 없으면 이상하다」는 현장마다 다르다. 1초마다 값을 보내는 계측기와 하루 한 번
127
+ * 올라오는 점검 기록이 같은 표에 있다. 기준을 하나로 정하면 앞쪽에는 신호가 너무 많이 서고
128
+ * 뒤쪽에는 서지 않는다. 그래서 그 설비가 지금까지 보여 준 가장 긴 간격을 기준으로 쓴다.
129
+ *
130
+ * 지난 간격을 아직 모르는 설비는 신호를 내지 않는다. 조치도 붙이지 않는다 — 고쳐야 할 것이
131
+ * 설비인지 연결인지 우리가 알 수 없다.
132
+ */
133
+ if (m.silentMs !== undefined && m.longestSilenceBeforeMs !== undefined && m.silentMs > m.longestSilenceBeforeMs) {
134
+ out.push({
135
+ id: `silent:${m.id}`,
136
+ kind: 'equipment-silent',
137
+ severity: 'medium',
138
+ anchor: { moverId: m.id, locationId: m.location },
139
+ params: {
140
+ moverId: m.id,
141
+ ...(m.location ? { locationId: m.location } : {}),
142
+ silentMs: m.silentMs,
143
+ longestBeforeMs: m.longestSilenceBeforeMs,
144
+ ...(m.status ? { lastStatus: m.status } : {})
145
+ }
146
+ });
147
+ }
109
148
  }
110
149
  for (const n of view.locations) {
111
150
  /*
@@ -321,14 +360,27 @@ export function computeOee(c, nowMs) {
321
360
  * 에너지 능력이 선언한 상태 필드를 스냅샷에 싣는다 — **온 것만.**
322
361
  *
323
362
  * 능력 계약이 이 필드들을 관측으로 선언해 두었는데 투영이 싣지 않으면, 값이 커널 안에서만 살고
324
- * 화면·성과·저널 어디에도 나타나지 않는다(실제로 그랬다: 값을 적었는데 스냅샷에 없었다).
363
+ * 화면·성과·지난 기록 어디에도 나타나지 않는다(실제로 그랬다: 값을 적었는데 스냅샷에 없었다).
325
364
  *
326
365
  * 값이 없으면 **키를 만들지 않는다** — 에너지에서 「모른다」와 「0 이다」는 요금이 갈리는 차이다.
366
+ *
367
+ * ── 목록을 손으로 적지 않는다 (2026-08-26) ─────────────────────────────────
368
+ * 여기가 필드 이름을 하드코딩하고 있었다. 그래서 능력에 필드를 하나 더하면 이 줄도 함께 고쳐야 했고,
369
+ * 잊으면 **바로 위 주석이 말하는 그 결함**이 다시 난다 — 실제로 그렇게 났다: 발전 적산
370
+ * (`generatedKWh`)을 능력에 선언하고 값을 적었는데 그때의 상태에 그 값이 없었다.
371
+ *
372
+ * 그래서 **선언에서 뽑는다.** 능력이 정본이고(`CAPABILITIES[*].stateFields`) 이 함수는 그것을 따른다.
373
+ * `measuredAt` 만 손으로 더한다 — 그것은 어느 능력의 값도 아니라 **언제 들었나**이고, 에너지 상태
374
+ * 필드 전체에 붙는 사실이다.
327
375
  */
376
+ const ENERGY_STATE_FIELDS = [
377
+ ...stateFieldsOf(['metered', 'curtailable', 'energyGenerating', 'energyStoring', 'switching']),
378
+ 'measuredAt'
379
+ ];
328
380
  function energyFieldsOf(m) {
329
381
  const out = {};
330
382
  const src = m;
331
- for (const k of ['generatedKW', 'exportKW', 'soc', 'chargeKW', 'dischargeKW', 'curtailable', 'minKW', 'position', 'measuredAt']) {
383
+ for (const k of ENERGY_STATE_FIELDS) {
332
384
  if (src[k] !== undefined)
333
385
  out[k] = src[k];
334
386
  }
@@ -599,7 +651,7 @@ export class FlowEngine {
599
651
  * 양품률 시임(선택) — **이력에서 배운 수율.** 소요시간과 같은 자리·같은 규율이다 (2026-08-19).
600
652
  *
601
653
  * 없으면 선언값(현장→원천), 그것도 없으면 도메인 상수. 무엇을 썼는지는 `specCoverage()` 가 밝힌다.
602
- * 주입은 호스트가 한다(저널을 접는 쪽이 이력을 안다) — 커널은 시임만 든다.
654
+ * 주입은 호스트가 한다(저널을 계산하는 쪽이 이력을 안다) — 커널은 시임만 든다.
603
655
  */
604
656
  yieldEstimator;
605
657
  /**
@@ -621,7 +673,7 @@ export class FlowEngine {
621
673
  * 읽는 쪽(`paramNumber`·`paramDuration`)이 명세 행보다 이것을 먼저 본다.
622
674
  */
623
675
  localParams = new Map();
624
- /** 관측 구동(P0) — 이벤트를 접는 투영기와 그 사실. tick 과 섞이지 않게 명시적으로 들고 있다. */
676
+ /** 관측 구동(P0) — 이벤트를 계산하는 투영기와 그 사실. tick 과 섞이지 않게 명시적으로 들고 있다. */
625
677
  observer;
626
678
  /**
627
679
  * **관측 리듀서의 재개점을 꺼낸다** — 호스트가 저장해 다음 기동에서 되돌릴 수 있게.
@@ -905,7 +957,7 @@ export class FlowEngine {
905
957
  * 미러의 진실은 원천이다. 그래서 재고·위치·설비 상태는 스냅샷에서 심지 않는다 — 다음 계측·다음
906
958
  * 마스터 재읽기가 곧 정정하므로, 심으면 **떠난 물건이 되살아나는** 위험만 남는다.
907
959
  *
908
- * 그런데 원천이 **애초에 다시 말해 주지 않는 축**이 있다. 우리가 접어 만든 것들이다:
960
+ * 그런데 원천이 **애초에 다시 말해 주지 않는 축**이 있다. 우리가 계산해 만든 것들이다:
909
961
  * · 확인(ack)·조건이 처음 성립한 시각 — 사람이 한 일과 우리가 기억한 시각
910
962
  * · 열린 구간의 누적·적산 기준점·관측 이후 최대 — SCADA 는 「이번 15분에 지금까지 얼마」를 모른다
911
963
  *
@@ -953,7 +1005,7 @@ export class FlowEngine {
953
1005
  * 미러는 그 작업을 굴리지 않는다 — **들은 것을 말할 뿐이다.** 그런데 규칙이 하나였으므로 관측된
954
1006
  * 사실이 씨앗 규칙에 걸려 사라졌다.
955
1007
  *
956
- * 실측: 포천 미러의 저널에 `task.status` 5,786건이 있고 투영기로 접으면 작업 2,881건인데
1008
+ * 실측: 포천 미러의 저널에 `task.status` 5,786건이 있고 투영기로 계산하면 작업 2,881건인데
957
1009
  * `getSnapshot().tasks` 는 **0** 이었다. 화면은 「아직 하나도 없습니다」라고 말했고 사용자가
958
1010
  * 반나절을 찾았다. 재현하면 관측기 5건 → 커널 0건이고, 걸린 조건은 오더였다.
959
1011
  *
@@ -977,7 +1029,7 @@ export class FlowEngine {
977
1029
  }
978
1030
  this.items.clear();
979
1031
  for (const it of snap.items) {
980
- /* **부분마다 한 줄로 주입한다** — `epc` 로 키를 잡으면 같은 로트의 두 부분이 하나로 접혀
1032
+ /* **부분마다 한 줄로 주입한다** — `epc` 로 키를 잡으면 같은 로트의 두 부분이 하나로 합쳐져
981
1033
  씨앗에서 재고가 줄어든다(§MaterialSubLot 에서 겪은 것과 같은 오류의 세 번째 자리). */
982
1034
  this.items.set(itemKeyOf(it), {
983
1035
  /* 품번 키·로트는 식별자에서 파생되므로 주입하지 않는다(스냅샷이 다시 낸다 — 두 벌을 두면 어긋난다). */
@@ -1040,6 +1092,20 @@ export class FlowEngine {
1040
1092
  ...(m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}),
1041
1093
  /* 결과도 이어받는다 — 잃으면 예측이 **자격 만료를 모르는 현장**에서 출발한다(낙관 쪽으로 치우친다). */
1042
1094
  ...(m.testResults ? { testResults: m.testResults } : {}),
1095
+ /*
1096
+ * ── 에너지 값도 이어받는다 (2026-08-26) ────────────────────────────────
1097
+ *
1098
+ * 이 자리가 에너지 필드를 심지 않았다. 그래서 재기동하면 설비의 발전량·충전율·계량값이 빈 채로
1099
+ * 시작하고, 다음 표본이 올 때까지 화면이 비어 보였다. 태양광 발전소에서 관측했다 — 저장된
1100
+ * 상태에는 `generatedKW` 가 있는데 되살린 트윈에는 없었다.
1101
+ *
1102
+ * 어느 필드를 이어받는지는 **능력 선언에서 뽑는다**(`ENERGY_STATE_FIELDS`). 스냅샷을 만드는
1103
+ * 쪽(`energyFieldsOf`)과 같은 목록이다 — 두 곳에 손으로 적으면 한쪽이 빠진 날 값이 조용히
1104
+ * 사라진다. 실제로 오늘 그렇게 났다.
1105
+ *
1106
+ * 값이 없으면 키를 만들지 않는다 — 에너지에서 「모른다」와 「0 이다」는 요금이 갈리는 차이다.
1107
+ */
1108
+ ...energyFieldsOf(m),
1043
1109
  ...effectiveOnly(m)
1044
1110
  });
1045
1111
  }
@@ -1370,7 +1436,7 @@ export class FlowEngine {
1370
1436
  */
1371
1437
  _acked = new Set();
1372
1438
  /**
1373
- * 완료 시점에 **주체가 사라져 접은 작업 수** — 조용한 누락이 되지 않게 센다.
1439
+ * 완료 시점에 **주체가 사라져 제외한 작업 수** — 조용한 누락이 되지 않게 센다.
1374
1440
  *
1375
1441
  * 물품이 포장·출하·소비로 없어지는 것은 정상이지만, 그 물품을 향한 작업이 남아 있었다는 것은
1376
1442
  * 상류에 어긋남이 있다는 신호다. 0 이 아니면 그 사실을 소비처가 볼 수 있어야 한다.
@@ -1773,8 +1839,21 @@ export class FlowEngine {
1773
1839
  criteriaOf.set(n.id, cs);
1774
1840
  }
1775
1841
  }
1842
+ /*
1843
+ * 소식이 끊긴 시간은 상태에 없고 받은 기록에만 있으므로 리듀서에게 묻는다. 스스로 상태를 만들어
1844
+ * 가는 트윈에는 그 리듀서가 없고, 그때는 값을 넣지 않으므로 이 신호도 서지 않는다.
1845
+ *
1846
+ * 앞날을 계산하는 사본(`fork`)에서도 묻지 않는다. 사본은 `structuredClone` 으로 만들어서 리듀서가
1847
+ * 메서드 없는 객체가 되고(§`ItemStore.clone`), 그대로 부르면 오류가 난다. 실제로 테스트 네 건이
1848
+ * 그렇게 실패했다. 다만 결과는 맞다 — 앞날 계산은 자기 시계로 진행하므로 소식이 끊긴다는 것이
1849
+ * 없다. 소식이 끊긴 사실은 현장을 따라가는 트윈이 알리고, 앞날 계산은 그 뒤의 일이다.
1850
+ */
1851
+ const silence = typeof this.observer?.equipmentSilence === 'function' ? this.observer.equipmentSilence() : undefined;
1776
1852
  const out = deriveAttentions({
1777
- equipment: [...this.equipment.values()],
1853
+ equipment: [...this.equipment.values()].map(m => {
1854
+ const s = silence?.get(m.id);
1855
+ return s ? { ...m, silentMs: s.silentMs, longestSilenceBeforeMs: s.longestBeforeMs } : m;
1856
+ }),
1778
1857
  locations: [...this.locations.values()].map(n => {
1779
1858
  const criteria = criteriaOf.get(n.id);
1780
1859
  return criteria?.length ? { ...n, criteria } : n;
@@ -1985,7 +2064,7 @@ export class FlowEngine {
1985
2064
  * 어긋난다(2026-08-01 하루에 아홉 곳). 근본 해법은 **한 상태 모델 두 구동**이고, 이것은 그 실현
1986
2065
  * 가능성을 재는 스파이크다(design/plans/kernel-unification-live-observe.md P0).
1987
2066
  *
1988
- * 여기서는 **이미 검증된 조각을 조립**한다: 투영기가 이벤트를 접고, 그 결과를 씨앗 경로
2067
+ * 여기서는 **이미 검증된 조각을 조립**한다: 투영기가 이벤트를 계산하고, 그 결과를 씨앗 경로
1989
2068
  * (`hydrateObserved`)로 커널 상태에 주입한다. 그래서 관측으로 실행한 커널을 그대로 `fork`·`tick` 할 수
1990
2069
  * 있다 — "미러에서 예측한다" 가 별도 배관 없이 성립하는지가 이 스파이크의 질문이다.
1991
2070
  *
@@ -3109,7 +3188,7 @@ export class FlowEngine {
3109
3188
  *
3110
3189
  * 그때 도메인 훅은 없는 것을 딛으려다 오류를 낸다(`order.gtin` · `item.location`). 그 예외 하나가
3111
3190
  * **예측 전체를 죽였다** — 사용자에게는 기능이 통째로 사라진 것으로 보였다. 그래서 완료 **전에**
3112
- * 여기서 묻고, 없으면 그 작업만 접는다.
3191
+ * 여기서 묻고, 없으면 그 작업만 제외한다.
3113
3192
  */
3114
3193
  missingContextOf(t) {
3115
3194
  if (t.itemEpc && !this.itemByRef(t.itemEpc))
@@ -3781,7 +3860,7 @@ export class FlowEngine {
3781
3860
  * 들어갈 때 없어지고 나올 때 생긴다.
3782
3861
  */
3783
3862
  /*
3784
- * **주체가 사라진 작업은 완료시키지 않고 접는다.**
3863
+ * **주체가 사라진 작업은 완료시키지 않고 계산한다.**
3785
3864
  *
3786
3865
  * 진행 중에 그 물품이 없어질 수 있다(포장으로 합쳐지고, 출하로 나가고, 다른 공정이 소비한다).
3787
3866
  * 그때 도메인 훅은 "없는 물품을 옮기려" 하다 오류를 냈고, **그 예외 하나가 예측 전체를 죽였다** —
package/dist/index.d.ts CHANGED
@@ -29,10 +29,10 @@ export { WmsKernel } from './kernel.ts';
29
29
  export { YmsKernel } from './yms-kernel.ts';
30
30
  export { MesKernel } from './mes-kernel.ts';
31
31
  export { EmsKernel, DEMAND_WINDOW_MS, demandWindowStart } from './ems-kernel.ts';
32
- export { ingestEnergyRecords, isEnergyRecord, ingestEnergyEquipmentRecords, isEnergyEquipmentRecord } from './energy-ingest.ts';
32
+ export { ingestEnergyRecords, isEnergyRecord, ingestEnergyEquipmentRecords, isEnergyEquipmentRecord, ingestEnergyGenerationRecords, isEnergyGenerationRecord } from './energy-ingest.ts';
33
33
  export { ingestOperationalRecords, isOperationalRecord, operationalKindOf } from './operational-ingest.ts';
34
34
  export type { OperationalKind, OperationalRecord, OperationalIngestOptions } from './operational-ingest.ts';
35
35
  export { attributeEnergy, electricityCost, energyIntensity, energyOfWindows } from './energy-attribution.ts';
36
36
  export type { AttributionBasis, AttributionResult, ElectricityCost, EnergyConsumer, EnergyPool, EnergyShare, IntensityInput, IntensityResult, IntensityDenominator, TariffDeclaration, WeightKind, WindowedEnergy } from './energy-attribution.ts';
37
- export type { EnergyRecord, EnergyEquipmentRecord, EnergyIngestOptions, EnergyIngestResult } from './energy-ingest.ts';
37
+ export type { EnergyRecord, EnergyEquipmentRecord, EnergyGenerationRecord, EnergyIngestOptions, EnergyIngestResult } from './energy-ingest.ts';
38
38
  export * from './vocabulary.ts';