@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.
@@ -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
  * 경계에서 사라진 자원은 결과에 실어 보낸다 — 수가 줄어든 것을 사용자가 눈치채지 못하면 안 된다.
@@ -550,7 +550,7 @@ export declare abstract class FlowEngine implements TwinKernel {
550
550
  * 양품률 시임(선택) — **이력에서 배운 수율.** 소요시간과 같은 자리·같은 규율이다 (2026-08-19).
551
551
  *
552
552
  * 없으면 선언값(현장→원천), 그것도 없으면 도메인 상수. 무엇을 썼는지는 `specCoverage()` 가 밝힌다.
553
- * 주입은 호스트가 한다(저널을 접는 쪽이 이력을 안다) — 커널은 시임만 든다.
553
+ * 주입은 호스트가 한다(저널을 계산하는 쪽이 이력을 안다) — 커널은 시임만 든다.
554
554
  */
555
555
  yieldEstimator?: YieldEstimator;
556
556
  /**
@@ -572,7 +572,7 @@ export declare abstract class FlowEngine implements TwinKernel {
572
572
  * 읽는 쪽(`paramNumber`·`paramDuration`)이 명세 행보다 이것을 먼저 본다.
573
573
  */
574
574
  protected localParams: Map<string, Map<string, string>>;
575
- /** 관측 구동(P0) — 이벤트를 접는 투영기와 그 사실. tick 과 섞이지 않게 명시적으로 들고 있다. */
575
+ /** 관측 구동(P0) — 이벤트를 계산하는 투영기와 그 사실. tick 과 섞이지 않게 명시적으로 들고 있다. */
576
576
  private observer?;
577
577
  /**
578
578
  * **관측 리듀서의 재개점을 꺼낸다** — 호스트가 저장해 다음 기동에서 되돌릴 수 있게.
@@ -745,7 +745,7 @@ export declare abstract class FlowEngine implements TwinKernel {
745
745
  * 미러의 진실은 원천이다. 그래서 재고·위치·설비 상태는 스냅샷에서 심지 않는다 — 다음 계측·다음
746
746
  * 마스터 재읽기가 곧 정정하므로, 심으면 **떠난 물건이 되살아나는** 위험만 남는다.
747
747
  *
748
- * 그런데 원천이 **애초에 다시 말해 주지 않는 축**이 있다. 우리가 접어 만든 것들이다:
748
+ * 그런데 원천이 **애초에 다시 말해 주지 않는 축**이 있다. 우리가 계산해 만든 것들이다:
749
749
  * · 확인(ack)·조건이 처음 성립한 시각 — 사람이 한 일과 우리가 기억한 시각
750
750
  * · 열린 구간의 누적·적산 기준점·관측 이후 최대 — SCADA 는 「이번 15분에 지금까지 얼마」를 모른다
751
751
  *
@@ -800,7 +800,7 @@ export declare abstract class FlowEngine implements TwinKernel {
800
800
  * 미러는 그 작업을 굴리지 않는다 — **들은 것을 말할 뿐이다.** 그런데 규칙이 하나였으므로 관측된
801
801
  * 사실이 씨앗 규칙에 걸려 사라졌다.
802
802
  *
803
- * 실측: 포천 미러의 저널에 `task.status` 5,786건이 있고 투영기로 접으면 작업 2,881건인데
803
+ * 실측: 포천 미러의 저널에 `task.status` 5,786건이 있고 투영기로 계산하면 작업 2,881건인데
804
804
  * `getSnapshot().tasks` 는 **0** 이었다. 화면은 「아직 하나도 없습니다」라고 말했고 사용자가
805
805
  * 반나절을 찾았다. 재현하면 관측기 5건 → 커널 0건이고, 걸린 조건은 오더였다.
806
806
  *
@@ -835,7 +835,7 @@ export declare abstract class FlowEngine implements TwinKernel {
835
835
  */
836
836
  private _acked;
837
837
  /**
838
- * 완료 시점에 **주체가 사라져 접은 작업 수** — 조용한 누락이 되지 않게 센다.
838
+ * 완료 시점에 **주체가 사라져 제외한 작업 수** — 조용한 누락이 되지 않게 센다.
839
839
  *
840
840
  * 물품이 포장·출하·소비로 없어지는 것은 정상이지만, 그 물품을 향한 작업이 남아 있었다는 것은
841
841
  * 상류에 어긋남이 있다는 신호다. 0 이 아니면 그 사실을 소비처가 볼 수 있어야 한다.
@@ -955,7 +955,7 @@ export declare abstract class FlowEngine implements TwinKernel {
955
955
  * 어긋난다(2026-08-01 하루에 아홉 곳). 근본 해법은 **한 상태 모델 두 구동**이고, 이것은 그 실현
956
956
  * 가능성을 재는 스파이크다(design/plans/kernel-unification-live-observe.md P0).
957
957
  *
958
- * 여기서는 **이미 검증된 조각을 조립**한다: 투영기가 이벤트를 접고, 그 결과를 씨앗 경로
958
+ * 여기서는 **이미 검증된 조각을 조립**한다: 투영기가 이벤트를 계산하고, 그 결과를 씨앗 경로
959
959
  * (`hydrateObserved`)로 커널 상태에 주입한다. 그래서 관측으로 실행한 커널을 그대로 `fork`·`tick` 할 수
960
960
  * 있다 — "미러에서 예측한다" 가 별도 배관 없이 성립하는지가 이 스파이크의 질문이다.
961
961
  *
@@ -1434,7 +1434,7 @@ export declare abstract class FlowEngine implements TwinKernel {
1434
1434
  *
1435
1435
  * 그때 도메인 훅은 없는 것을 딛으려다 오류를 낸다(`order.gtin` · `item.location`). 그 예외 하나가
1436
1436
  * **예측 전체를 죽였다** — 사용자에게는 기능이 통째로 사라진 것으로 보였다. 그래서 완료 **전에**
1437
- * 여기서 묻고, 없으면 그 작업만 접는다.
1437
+ * 여기서 묻고, 없으면 그 작업만 제외한다.
1438
1438
  */
1439
1439
  protected missingContextOf(t: FlowTask): 'item' | 'order' | 'domain' | undefined;
1440
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
  * 선언한 유효 기간만 뽑는다 — 마스터가 말하지 않으면 필드를 만들지 않는다.
@@ -358,14 +360,27 @@ export function computeOee(c, nowMs) {
358
360
  * 에너지 능력이 선언한 상태 필드를 스냅샷에 싣는다 — **온 것만.**
359
361
  *
360
362
  * 능력 계약이 이 필드들을 관측으로 선언해 두었는데 투영이 싣지 않으면, 값이 커널 안에서만 살고
361
- * 화면·성과·저널 어디에도 나타나지 않는다(실제로 그랬다: 값을 적었는데 스냅샷에 없었다).
363
+ * 화면·성과·지난 기록 어디에도 나타나지 않는다(실제로 그랬다: 값을 적었는데 스냅샷에 없었다).
362
364
  *
363
365
  * 값이 없으면 **키를 만들지 않는다** — 에너지에서 「모른다」와 「0 이다」는 요금이 갈리는 차이다.
366
+ *
367
+ * ── 목록을 손으로 적지 않는다 (2026-08-26) ─────────────────────────────────
368
+ * 여기가 필드 이름을 하드코딩하고 있었다. 그래서 능력에 필드를 하나 더하면 이 줄도 함께 고쳐야 했고,
369
+ * 잊으면 **바로 위 주석이 말하는 그 결함**이 다시 난다 — 실제로 그렇게 났다: 발전 적산
370
+ * (`generatedKWh`)을 능력에 선언하고 값을 적었는데 그때의 상태에 그 값이 없었다.
371
+ *
372
+ * 그래서 **선언에서 뽑는다.** 능력이 정본이고(`CAPABILITIES[*].stateFields`) 이 함수는 그것을 따른다.
373
+ * `measuredAt` 만 손으로 더한다 — 그것은 어느 능력의 값도 아니라 **언제 들었나**이고, 에너지 상태
374
+ * 필드 전체에 붙는 사실이다.
364
375
  */
376
+ const ENERGY_STATE_FIELDS = [
377
+ ...stateFieldsOf(['metered', 'curtailable', 'energyGenerating', 'energyStoring', 'switching']),
378
+ 'measuredAt'
379
+ ];
365
380
  function energyFieldsOf(m) {
366
381
  const out = {};
367
382
  const src = m;
368
- for (const k of ['generatedKW', 'exportKW', 'soc', 'chargeKW', 'dischargeKW', 'curtailable', 'minKW', 'position', 'measuredAt']) {
383
+ for (const k of ENERGY_STATE_FIELDS) {
369
384
  if (src[k] !== undefined)
370
385
  out[k] = src[k];
371
386
  }
@@ -636,7 +651,7 @@ export class FlowEngine {
636
651
  * 양품률 시임(선택) — **이력에서 배운 수율.** 소요시간과 같은 자리·같은 규율이다 (2026-08-19).
637
652
  *
638
653
  * 없으면 선언값(현장→원천), 그것도 없으면 도메인 상수. 무엇을 썼는지는 `specCoverage()` 가 밝힌다.
639
- * 주입은 호스트가 한다(저널을 접는 쪽이 이력을 안다) — 커널은 시임만 든다.
654
+ * 주입은 호스트가 한다(저널을 계산하는 쪽이 이력을 안다) — 커널은 시임만 든다.
640
655
  */
641
656
  yieldEstimator;
642
657
  /**
@@ -658,7 +673,7 @@ export class FlowEngine {
658
673
  * 읽는 쪽(`paramNumber`·`paramDuration`)이 명세 행보다 이것을 먼저 본다.
659
674
  */
660
675
  localParams = new Map();
661
- /** 관측 구동(P0) — 이벤트를 접는 투영기와 그 사실. tick 과 섞이지 않게 명시적으로 들고 있다. */
676
+ /** 관측 구동(P0) — 이벤트를 계산하는 투영기와 그 사실. tick 과 섞이지 않게 명시적으로 들고 있다. */
662
677
  observer;
663
678
  /**
664
679
  * **관측 리듀서의 재개점을 꺼낸다** — 호스트가 저장해 다음 기동에서 되돌릴 수 있게.
@@ -942,7 +957,7 @@ export class FlowEngine {
942
957
  * 미러의 진실은 원천이다. 그래서 재고·위치·설비 상태는 스냅샷에서 심지 않는다 — 다음 계측·다음
943
958
  * 마스터 재읽기가 곧 정정하므로, 심으면 **떠난 물건이 되살아나는** 위험만 남는다.
944
959
  *
945
- * 그런데 원천이 **애초에 다시 말해 주지 않는 축**이 있다. 우리가 접어 만든 것들이다:
960
+ * 그런데 원천이 **애초에 다시 말해 주지 않는 축**이 있다. 우리가 계산해 만든 것들이다:
946
961
  * · 확인(ack)·조건이 처음 성립한 시각 — 사람이 한 일과 우리가 기억한 시각
947
962
  * · 열린 구간의 누적·적산 기준점·관측 이후 최대 — SCADA 는 「이번 15분에 지금까지 얼마」를 모른다
948
963
  *
@@ -990,7 +1005,7 @@ export class FlowEngine {
990
1005
  * 미러는 그 작업을 굴리지 않는다 — **들은 것을 말할 뿐이다.** 그런데 규칙이 하나였으므로 관측된
991
1006
  * 사실이 씨앗 규칙에 걸려 사라졌다.
992
1007
  *
993
- * 실측: 포천 미러의 저널에 `task.status` 5,786건이 있고 투영기로 접으면 작업 2,881건인데
1008
+ * 실측: 포천 미러의 저널에 `task.status` 5,786건이 있고 투영기로 계산하면 작업 2,881건인데
994
1009
  * `getSnapshot().tasks` 는 **0** 이었다. 화면은 「아직 하나도 없습니다」라고 말했고 사용자가
995
1010
  * 반나절을 찾았다. 재현하면 관측기 5건 → 커널 0건이고, 걸린 조건은 오더였다.
996
1011
  *
@@ -1014,7 +1029,7 @@ export class FlowEngine {
1014
1029
  }
1015
1030
  this.items.clear();
1016
1031
  for (const it of snap.items) {
1017
- /* **부분마다 한 줄로 주입한다** — `epc` 로 키를 잡으면 같은 로트의 두 부분이 하나로 접혀
1032
+ /* **부분마다 한 줄로 주입한다** — `epc` 로 키를 잡으면 같은 로트의 두 부분이 하나로 합쳐져
1018
1033
  씨앗에서 재고가 줄어든다(§MaterialSubLot 에서 겪은 것과 같은 오류의 세 번째 자리). */
1019
1034
  this.items.set(itemKeyOf(it), {
1020
1035
  /* 품번 키·로트는 식별자에서 파생되므로 주입하지 않는다(스냅샷이 다시 낸다 — 두 벌을 두면 어긋난다). */
@@ -1077,6 +1092,20 @@ export class FlowEngine {
1077
1092
  ...(m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}),
1078
1093
  /* 결과도 이어받는다 — 잃으면 예측이 **자격 만료를 모르는 현장**에서 출발한다(낙관 쪽으로 치우친다). */
1079
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),
1080
1109
  ...effectiveOnly(m)
1081
1110
  });
1082
1111
  }
@@ -1407,7 +1436,7 @@ export class FlowEngine {
1407
1436
  */
1408
1437
  _acked = new Set();
1409
1438
  /**
1410
- * 완료 시점에 **주체가 사라져 접은 작업 수** — 조용한 누락이 되지 않게 센다.
1439
+ * 완료 시점에 **주체가 사라져 제외한 작업 수** — 조용한 누락이 되지 않게 센다.
1411
1440
  *
1412
1441
  * 물품이 포장·출하·소비로 없어지는 것은 정상이지만, 그 물품을 향한 작업이 남아 있었다는 것은
1413
1442
  * 상류에 어긋남이 있다는 신호다. 0 이 아니면 그 사실을 소비처가 볼 수 있어야 한다.
@@ -2035,7 +2064,7 @@ export class FlowEngine {
2035
2064
  * 어긋난다(2026-08-01 하루에 아홉 곳). 근본 해법은 **한 상태 모델 두 구동**이고, 이것은 그 실현
2036
2065
  * 가능성을 재는 스파이크다(design/plans/kernel-unification-live-observe.md P0).
2037
2066
  *
2038
- * 여기서는 **이미 검증된 조각을 조립**한다: 투영기가 이벤트를 접고, 그 결과를 씨앗 경로
2067
+ * 여기서는 **이미 검증된 조각을 조립**한다: 투영기가 이벤트를 계산하고, 그 결과를 씨앗 경로
2039
2068
  * (`hydrateObserved`)로 커널 상태에 주입한다. 그래서 관측으로 실행한 커널을 그대로 `fork`·`tick` 할 수
2040
2069
  * 있다 — "미러에서 예측한다" 가 별도 배관 없이 성립하는지가 이 스파이크의 질문이다.
2041
2070
  *
@@ -3159,7 +3188,7 @@ export class FlowEngine {
3159
3188
  *
3160
3189
  * 그때 도메인 훅은 없는 것을 딛으려다 오류를 낸다(`order.gtin` · `item.location`). 그 예외 하나가
3161
3190
  * **예측 전체를 죽였다** — 사용자에게는 기능이 통째로 사라진 것으로 보였다. 그래서 완료 **전에**
3162
- * 여기서 묻고, 없으면 그 작업만 접는다.
3191
+ * 여기서 묻고, 없으면 그 작업만 제외한다.
3163
3192
  */
3164
3193
  missingContextOf(t) {
3165
3194
  if (t.itemEpc && !this.itemByRef(t.itemEpc))
@@ -3831,7 +3860,7 @@ export class FlowEngine {
3831
3860
  * 들어갈 때 없어지고 나올 때 생긴다.
3832
3861
  */
3833
3862
  /*
3834
- * **주체가 사라진 작업은 완료시키지 않고 접는다.**
3863
+ * **주체가 사라진 작업은 완료시키지 않고 계산한다.**
3835
3864
  *
3836
3865
  * 진행 중에 그 물품이 없어질 수 있다(포장으로 합쳐지고, 출하로 나가고, 다른 공정이 소비한다).
3837
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';
package/dist/index.js CHANGED
@@ -29,8 +29,8 @@ export { WmsKernel } from "./kernel.js";
29
29
  export { YmsKernel } from "./yms-kernel.js";
30
30
  export { MesKernel } from "./mes-kernel.js";
31
31
  export { EmsKernel, DEMAND_WINDOW_MS, demandWindowStart } from "./ems-kernel.js";
32
- export { ingestEnergyRecords, isEnergyRecord, ingestEnergyEquipmentRecords, isEnergyEquipmentRecord } from "./energy-ingest.js";
33
- /* 운영 사실의 문 — 리듀서가 접는 여섯이 들어오는 자리(미러가 시뮬보다 가난하지 않게). */
32
+ export { ingestEnergyRecords, isEnergyRecord, ingestEnergyEquipmentRecords, isEnergyEquipmentRecord, ingestEnergyGenerationRecords, isEnergyGenerationRecord } from "./energy-ingest.js";
33
+ /* 운영 사실의 문 — 리듀서가 다루는 여섯이 들어오는 자리(미러가 시뮬보다 가난하지 않게). */
34
34
  export { ingestOperationalRecords, isOperationalRecord, operationalKindOf } from "./operational-ingest.js";
35
35
  export { attributeEnergy, electricityCost, energyIntensity, energyOfWindows } from "./energy-attribution.js";
36
36
  /* 에너지 상태 타입은 **계약**에 있다(상태의 모양은 계약이다) — contract 의 `export *` 가 이미 낸다. */
@@ -48,7 +48,7 @@ export interface JobResponse {
48
48
  /** 저널 한 줄에서 자원·자재 실적을 읽기 위한 최소 모양(작업 폴드가 보는 것과 같은 줄). */
49
49
  type Row = TaskDeltaRow;
50
50
  /**
51
- * 저널을 접어 **작업별 실적 회신**을 만든다 — 표준 이름으로.
51
+ * 저널을 계산해 **작업별 실적 회신**을 만든다 — 표준 이름으로.
52
52
  *
53
53
  * 짝맞춤(어느 전이가 한 작업인가)은 **커널 규칙**(`foldTaskRecords`)을 그대로 쓴다. 여기서 다시 짝을
54
54
  * 맞추면 같은 저널이 지표에서는 3건, 실적에서는 4건이 되는 일이 생긴다 — 실제로 겪은 부류다.
@@ -5,7 +5,7 @@ const dataOf = (r) => {
5
5
  return r.data ?? p?.data ?? p ?? {};
6
6
  };
7
7
  /**
8
- * 저널을 접어 **작업별 실적 회신**을 만든다 — 표준 이름으로.
8
+ * 저널을 계산해 **작업별 실적 회신**을 만든다 — 표준 이름으로.
9
9
  *
10
10
  * 짝맞춤(어느 전이가 한 작업인가)은 **커널 규칙**(`foldTaskRecords`)을 그대로 쓴다. 여기서 다시 짝을
11
11
  * 맞추면 같은 저널이 지표에서는 3건, 실적에서는 4건이 되는 일이 생긴다 — 실제로 겪은 부류다.
package/dist/kernel.js CHANGED
@@ -196,7 +196,7 @@ export class WmsKernel extends FlowEngine {
196
196
  if (t.intent === 'process')
197
197
  return this.onProcessComplete(t);
198
198
  const item = this.itemByRef(t.itemEpc);
199
- /* 여기 도달했다면 코어가 이미 물품을 확인했다(주체가 사라진 작업은 완료 전에 접힌다).
199
+ /* 여기 도달했다면 코어가 이미 물품을 확인했다(주체가 사라진 작업은 완료 전에 제외된다).
200
200
  그래도 단정(`!`)은 쓰지 않는다 — 계약이 바뀌면 조용히 틀리는 대신 분명히 멈춘다. */
201
201
  if (!item)
202
202
  throw new Error(`task ${t.id}: item "${t.itemEpc}" vanished between the core check and the domain hook`);
@@ -121,7 +121,7 @@ export declare class MesKernel extends FlowEngine {
121
121
  *
122
122
  * 씨앗이 이미 이행된 오더를 주입하지 않으므로(남은 수량 0) 그 오더에 딸린 작업만 남을 수 있고,
123
123
  * 관측이 오더 연결을 담지 못한 작업도 있다. 그때 예전에는 `order.gtin` 에서 오류가 났고 **그 예외
124
- * 하나가 정확도 추세 전체를 죽였다.** 이제 코어가 이 답을 보고 그 작업만 접는다.
124
+ * 하나가 정확도 추세 전체를 죽였다.** 이제 코어가 이 답을 보고 그 작업만 제외한다.
125
125
  */
126
126
  protected canComplete(t: FlowTask): boolean;
127
127
  protected onTaskComplete(t: FlowTask): void;
@@ -356,7 +356,7 @@ export class MesKernel extends FlowEngine {
356
356
  *
357
357
  * 씨앗이 이미 이행된 오더를 주입하지 않으므로(남은 수량 0) 그 오더에 딸린 작업만 남을 수 있고,
358
358
  * 관측이 오더 연결을 담지 못한 작업도 있다. 그때 예전에는 `order.gtin` 에서 오류가 났고 **그 예외
359
- * 하나가 정확도 추세 전체를 죽였다.** 이제 코어가 이 답을 보고 그 작업만 접는다.
359
+ * 하나가 정확도 추세 전체를 죽였다.** 이제 코어가 이 답을 보고 그 작업만 제외한다.
360
360
  */
361
361
  canComplete(t) {
362
362
  return !!(t.orderId && this.orders.get(t.orderId));
@@ -92,10 +92,10 @@ export interface ProjectedState {
92
92
  acked: string[];
93
93
  }
94
94
  /**
95
- * 리듀서의 **완전한 재개점** — 이어 접기의 씨앗.
95
+ * 리듀서의 **완전한 재개점** — 이어 계산의 씨앗.
96
96
  *
97
97
  * `ProjectedState`(보기)와 구별한다: 여기에는 소비처가 보지 않는 것도 들어간다(보류된 담김·집합·
98
- * 반영 못 한 사건 집계). 그것이 빠지면 「이어 접은 결과」가 「0부터 접은 결과」와 조용히 달라진다.
98
+ * 반영 못 한 사건 집계). 그것이 빠지면 「이어 계산한 결과」가 「0부터 계산한 결과」와 조용히 달라진다.
99
99
  */
100
100
  export interface ReducerCheckpoint {
101
101
  revision: number;
@@ -151,7 +151,7 @@ export interface ReducerCheckpoint {
151
151
  firstAtMs?: number;
152
152
  lastAtMs?: number;
153
153
  }[];
154
- /** 자리별 · 속성별 마지막 관측 — 이어 접기가 이 축을 0부터 다시 만들지 않게. */
154
+ /** 자리별 · 속성별 마지막 관측 — 이어 계산가 이 축을 0부터 다시 만들지 않게. */
155
155
  observations?: {
156
156
  id: string;
157
157
  values: LocationObservation[];
@@ -232,12 +232,12 @@ export declare class ObservedReducer {
232
232
  /**
233
233
  * **구조를 전환한다** — 관측된 사실은 지키고 토폴로지만 새 선언으로 바꾼다.
234
234
  *
235
- * 공장은 바뀐다. 도장 부스를 넷 더 놓고, 라인을 하나 접는다. 그런데 지금까지는 구조가 바뀌면
235
+ * 공장은 바뀐다. 도장 부스를 넷 더 놓고, 라인 하나를 닫는다. 그런데 지금까지는 구조가 바뀌면
236
236
  * **그 트윈의 저널을 통째로 지우는 것**이 유일한 길이었다 — 안 지우면 옛 이벤트를 새 공장에 대고
237
- * 접게 되어 이력이 거짓말을 한다. 역사를 잃거나 거짓말을 하거나, 둘뿐이었다.
237
+ * 계산하게 되어 이력이 거짓말을 한다. 역사를 잃거나 거짓말을 하거나, 둘뿐이었다.
238
238
  *
239
239
  * 셋째 길이 이것이다: 이벤트가 **자기 구조를 달고** 다니고, 재생은 구조가 바뀌는 지점에서 여기를
240
- * 불러 전환한 뒤 이어 접는다. 그러면 "그때 그 공장의 사실" 로 계속 읽힌다.
240
+ * 불러 전환한 뒤 이어서 계산한다. 그러면 "그때 그 공장의 사실" 로 계속 읽힌다.
241
241
  *
242
242
  * ── 무엇을 지키고 무엇을 버리는가 ───────────────────────────────────────
243
243
  * **관측은 지킨다** — 물품·오더·작업·집합은 구조와 무관한 사실이다(팔레트는 부스를 늘려도 그대로다).
@@ -357,7 +357,7 @@ export declare class ObservedReducer {
357
357
  * 시뮬 기준시각(BASE_EPOCH, 2026-01-01)에 얼어 있었다.
358
358
  *
359
359
  * 결과는 조용했다: 구간 마감·피크 같은 **커널이 낸 사실이 8개월 전으로 기록**되어, 저널을 기간으로
360
- * 접는 소비처(성과·요금·피더 배분)가 그 트윈에서 영원히 아무것도 찾지 못했다. 계측 자체는 원천 시각을
360
+ * 계산하는 소비처(성과·요금·피더 배분)가 그 트윈에서 영원히 아무것도 찾지 못했다. 계측 자체는 원천 시각을
361
361
  * 달고 있었으므로 화면의 순간값만 정상으로 보였다 — 그래서 눈에 띄지 않았다.
362
362
  *
363
363
  * 시각을 되돌리지는 않는다: 늦게 도착한 옛 표본이 "지금" 을 과거로 끌면 그 뒤 판정이 전부 흔들린다.
@@ -384,14 +384,14 @@ export declare class ObservedReducer {
384
384
  /**
385
385
  * **재개점(checkpoint)** — 스냅샷과 다르다.
386
386
  *
387
- * ── 왜 스냅샷으로는 이어 접을 수 없나 (2026-08-18) ─────────────────────────
387
+ * ── 왜 스냅샷으로는 이어 계산할 수 없나 (2026-08-18) ─────────────────────────
388
388
  * `snapshot()` 은 **소비처가 보는 값**이다(파생된 판정·정리된 목록). 그것으로 리듀서를 되세우면
389
389
  * 보이지 않는 것들이 사라진다 — 아직 등장하지 않은 자식의 담김(`pendingParent`), 집합 관계
390
390
  * (`aggregation`), 반영 못 한 사건 집계(`unhandled`), 정정 선언(`corrections`). 그 상태에서 뒤 이벤트를
391
- * 접으면 **0부터 접은 결과와 달라진다.** 다르면 그 차이는 조용하다(오류가 없다).
391
+ * 계산하면 **0부터 계산한 결과와 달라진다.** 다르면 그 차이는 조용하다(오류가 없다).
392
392
  *
393
- * 그래서 재개점은 **내부 상태 전부**다. 「이어 접기」의 정합성은 시험이 증명한다:
394
- * 0부터 접은 결과 == 앞부분 재개점 + 뒷부분 접기.
393
+ * 그래서 재개점은 **내부 상태 전부**다. 「이어 계산」의 정합성은 시험이 증명한다:
394
+ * 0부터 계산한 결과 == 앞부분 재개점 + 뒷부분 계산.
395
395
  *
396
396
  * 모델에서 오는 것(교대·등급·시간대)은 담지 않는다 — 되세울 때 같은 모델을 받기 때문이다.
397
397
  */
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Observed Reducer — **이벤트를 접어 상태를 만드는 단 하나의 규칙.**
2
+ * Observed Reducer — **이벤트를 계산해 상태를 만드는 단 하나의 규칙.**
3
3
  *
4
4
  * 이 파일은 원래 미러 전용(`StateProjector`)이었다. 그런데 커널도 이벤트로 굴러갈 수 있게 되면서
5
5
  * (`FlowEngine.apply`, 통합 P0) **두 구동이 같은 규칙을 써야** 한다 — 규칙이 두 벌이면 반드시
@@ -14,7 +14,7 @@
14
14
  * live 모드: 외부(실 WMS)에서 이벤트가 도착 → 이 projector 가 State 를 재구성.
15
15
  * → "계약 동일, 데이터원만 스왑"(execution-model.md §5, ADR-0010). 같은 이벤트면 같은 State.
16
16
  *
17
- * 두 갈래 이벤트를 함께 접는다:
17
+ * 두 갈래 이벤트를 함께 계산한다:
18
18
  * - EPCIS(epcis.*) → 재고/위치/조립 (What/Where)
19
19
  * - 운영 델타(task/equipment/order.status) → tasks·equipment·orders (EPCIS 로 재구성 불가한 절반)
20
20
  * 마스터(로케이션)는 board 초기화 + applyMaster 로 갱신(마스터 동기).
@@ -123,12 +123,12 @@ export class ObservedReducer {
123
123
  /**
124
124
  * **구조를 전환한다** — 관측된 사실은 지키고 토폴로지만 새 선언으로 바꾼다.
125
125
  *
126
- * 공장은 바뀐다. 도장 부스를 넷 더 놓고, 라인을 하나 접는다. 그런데 지금까지는 구조가 바뀌면
126
+ * 공장은 바뀐다. 도장 부스를 넷 더 놓고, 라인 하나를 닫는다. 그런데 지금까지는 구조가 바뀌면
127
127
  * **그 트윈의 저널을 통째로 지우는 것**이 유일한 길이었다 — 안 지우면 옛 이벤트를 새 공장에 대고
128
- * 접게 되어 이력이 거짓말을 한다. 역사를 잃거나 거짓말을 하거나, 둘뿐이었다.
128
+ * 계산하게 되어 이력이 거짓말을 한다. 역사를 잃거나 거짓말을 하거나, 둘뿐이었다.
129
129
  *
130
130
  * 셋째 길이 이것이다: 이벤트가 **자기 구조를 달고** 다니고, 재생은 구조가 바뀌는 지점에서 여기를
131
- * 불러 전환한 뒤 이어 접는다. 그러면 "그때 그 공장의 사실" 로 계속 읽힌다.
131
+ * 불러 전환한 뒤 이어서 계산한다. 그러면 "그때 그 공장의 사실" 로 계속 읽힌다.
132
132
  *
133
133
  * ── 무엇을 지키고 무엇을 버리는가 ───────────────────────────────────────
134
134
  * **관측은 지킨다** — 물품·오더·작업·집합은 구조와 무관한 사실이다(팔레트는 부스를 늘려도 그대로다).
@@ -460,7 +460,7 @@ export class ObservedReducer {
460
460
  }
461
461
  case OP_EVENT.test: {
462
462
  /*
463
- * **시험 결과** — 대상을 가리켜 들어오고(표준 방향), 상태에서는 그 개체 안에 접힌다.
463
+ * **시험 결과** — 대상을 가리켜 들어오고(표준 방향), 상태에서는 그 개체 안에 계산된다.
464
464
  *
465
465
  * 명세당 마지막 하나만 든다: 이력을 들면 상태가 계측 주기로 자란다(품목 100만 기준에서 벽이다).
466
466
  * 늦게 온 옛 결과가 최신을 덮지 않게 `stale` 을 지나며, **대상 키에 명세를 넣는다** — 온도
@@ -930,7 +930,7 @@ export class ObservedReducer {
930
930
  * 시뮬 기준시각(BASE_EPOCH, 2026-01-01)에 얼어 있었다.
931
931
  *
932
932
  * 결과는 조용했다: 구간 마감·피크 같은 **커널이 낸 사실이 8개월 전으로 기록**되어, 저널을 기간으로
933
- * 접는 소비처(성과·요금·피더 배분)가 그 트윈에서 영원히 아무것도 찾지 못했다. 계측 자체는 원천 시각을
933
+ * 계산하는 소비처(성과·요금·피더 배분)가 그 트윈에서 영원히 아무것도 찾지 못했다. 계측 자체는 원천 시각을
934
934
  * 달고 있었으므로 화면의 순간값만 정상으로 보였다 — 그래서 눈에 띄지 않았다.
935
935
  *
936
936
  * 시각을 되돌리지는 않는다: 늦게 도착한 옛 표본이 "지금" 을 과거로 끌면 그 뒤 판정이 전부 흔들린다.
@@ -993,20 +993,20 @@ export class ObservedReducer {
993
993
  /**
994
994
  * **재개점(checkpoint)** — 스냅샷과 다르다.
995
995
  *
996
- * ── 왜 스냅샷으로는 이어 접을 수 없나 (2026-08-18) ─────────────────────────
996
+ * ── 왜 스냅샷으로는 이어 계산할 수 없나 (2026-08-18) ─────────────────────────
997
997
  * `snapshot()` 은 **소비처가 보는 값**이다(파생된 판정·정리된 목록). 그것으로 리듀서를 되세우면
998
998
  * 보이지 않는 것들이 사라진다 — 아직 등장하지 않은 자식의 담김(`pendingParent`), 집합 관계
999
999
  * (`aggregation`), 반영 못 한 사건 집계(`unhandled`), 정정 선언(`corrections`). 그 상태에서 뒤 이벤트를
1000
- * 접으면 **0부터 접은 결과와 달라진다.** 다르면 그 차이는 조용하다(오류가 없다).
1000
+ * 계산하면 **0부터 계산한 결과와 달라진다.** 다르면 그 차이는 조용하다(오류가 없다).
1001
1001
  *
1002
- * 그래서 재개점은 **내부 상태 전부**다. 「이어 접기」의 정합성은 시험이 증명한다:
1003
- * 0부터 접은 결과 == 앞부분 재개점 + 뒷부분 접기.
1002
+ * 그래서 재개점은 **내부 상태 전부**다. 「이어 계산」의 정합성은 시험이 증명한다:
1003
+ * 0부터 계산한 결과 == 앞부분 재개점 + 뒷부분 계산.
1004
1004
  *
1005
1005
  * 모델에서 오는 것(교대·등급·시간대)은 담지 않는다 — 되세울 때 같은 모델을 받기 때문이다.
1006
1006
  */
1007
1007
  serialize() {
1008
1008
  return {
1009
- /* 관측도 재개점에 든다 — 없으면 이어 접기가 그 축을 0부터 다시 만든다. */
1009
+ /* 관측도 재개점에 든다 — 없으면 이어 계산가 그 축을 0부터 다시 만든다. */
1010
1010
  observations: [...this.observations.entries()].map(([id, bin]) => ({ id, values: [...bin.values()] })),
1011
1011
  revision: this.revision,
1012
1012
  master: [...this.master.values()].map(n => ({ ...n })),