@operato/twin-kernel 0.7.4 → 0.7.5

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.
@@ -1850,6 +1850,14 @@ export interface TwinKernel {
1850
1850
  adoptStructure(def: TwinModelDef): StructureShift;
1851
1851
  /** 이 커널이 관측으로 구동됨을 선언한다 — 첫 이벤트가 오기 전에도 그렇다. */
1852
1852
  observe(): void;
1853
+ /**
1854
+ * 시각 기준점을 세운다 — 이 트윈의 「지금」은 `기준점 + 경과`다.
1855
+ *
1856
+ * 세우는 쪽이 기동 순간의 실제 시각을 준다. 주지 않으면 커널의 기본 기준점을 쓰는데, 그러면 시뮬
1857
+ * 트윈의 저널 시각이 재기동마다 되감겨 「언제 있었던 일인가」를 되짚을 수 없다.
1858
+ * **기동 직후에만** 부른다 — 시계가 흐른 뒤에 옮기면 그전에 낸 사실들과 시간축이 어긋난다.
1859
+ */
1860
+ setClockOrigin?(originMs: number): void;
1853
1861
  getSnapshot(): StateSnapshot;
1854
1862
  onEvent(handler: EventHandler): Unsubscribe;
1855
1863
  dispatch(cmd: Command): CommandAck;
@@ -276,12 +276,23 @@ export class EmsKernel extends FlowEngine {
276
276
  * kW 를 못 읽은 경우(계기 오류·필드 누락)가 실제로 있고, 그것은 「아무것도 오지 않았다」와
277
277
  * 다르다. 무엇이 왔는지는 지점별 `samplesInWindow` 가 답한다.
278
278
  */
279
- ...(w.samples === 0 ? { observedAbsence: 'no-load-samples' } : {})
279
+ ...(w.samples === 0 ? { observedAbsence: 'no-load-samples' } : {}),
280
+ /*
281
+ * **만든 값이면 그렇게 말한다.** 상태에만 표시하고 사실에는 빠뜨리면, 저널을 읽는 쪽
282
+ * (성과·이력·보고서)이 시뮬레이션의 수를 계측으로 읽는다 — 값이 그럴듯할수록 위험하다.
283
+ */
284
+ ...(w.derived ? { derived: true } : {})
280
285
  });
281
286
  /* 피크는 **마감된 구간**으로만 갱신한다 — 열린 구간의 최대는 아직 확정이 아니다. */
282
287
  if (w.maxKW !== undefined && (this.peak === undefined || w.maxKW > this.peak.kW)) {
283
288
  this.peak = { kW: w.maxKW, windowStartMs: w.startMs };
284
- this.emitOp(ENERGY_EVENT.peak, { kW: w.maxKW, windowStartMs: w.startMs, ...(w.contractKW !== undefined ? { contractKW: w.contractKW } : {}) });
289
+ this.emitOp(ENERGY_EVENT.peak, {
290
+ kW: w.maxKW,
291
+ windowStartMs: w.startMs,
292
+ ...(w.contractKW !== undefined ? { contractKW: w.contractKW } : {}),
293
+ /* 피크도 그 구간에서 왔다 — 구간이 만든 값이면 피크도 만든 값이다. */
294
+ ...(w.derived ? { derived: true } : {})
295
+ });
285
296
  }
286
297
  /* 다음 구간은 표본이 올 때 연다 — 미리 열면 오지 않은 구간을 존재하는 것처럼 만든다. */
287
298
  this.open = undefined;
@@ -419,9 +430,18 @@ export class EmsKernel extends FlowEngine {
419
430
  }
420
431
  tick(dtMs) {
421
432
  super.tick(dtMs);
422
- /* 부하를 먼저 만들고 마감한다 — 마감이 먼저면 마지막 값이 다음 구간으로 밀린다. */
423
- this.deriveLoad(this.clockMs);
424
- this.closeDue(this.clockMs);
433
+ /*
434
+ * 구간은 **이 트윈의 「지금」**으로 잡는다(`nowMs`) — 경과 시간(`clockMs`)이 아니다.
435
+ *
436
+ * 예전에는 경과 시간을 그대로 넘겨서 시뮬 트윈의 수요 구간이 **1970년**에 섰다. 계측 경로는
437
+ * 계측이 말한 절대 시각을 쓰므로, 두 경로가 서로 다른 시간축에 값을 넣고 있었던 셈이다.
438
+ * 요금은 벽시계의 15분으로 매겨지니 그 경계에 서야 견줄 수 있다.
439
+ *
440
+ * 부하를 먼저 만들고 마감한다 — 마감이 먼저면 마지막 값이 다음 구간으로 밀린다.
441
+ */
442
+ const at = this.nowMs();
443
+ this.deriveLoad(at);
444
+ this.closeDue(at);
425
445
  }
426
446
  getSnapshot() {
427
447
  const snap = super.getSnapshot();
@@ -285,6 +285,20 @@ export declare abstract class FlowEngine implements TwinKernel {
285
285
  orders: Map<string, FlowOrder>;
286
286
  revision: number;
287
287
  clockMs: number;
288
+ /**
289
+ * 시각의 **기준점** — 이 트윈의 「지금」은 `originMs + clockMs` 다.
290
+ *
291
+ * ── 왜 고정 상수가 아닌가 (2026-08-15) ─────────────────────────────────────
292
+ * 기준점이 `BASE_EPOCH` 하나로 고정돼 있어서, 시뮬 트윈의 저널 시각이 **재기동마다 되감겼다.**
293
+ * 며칠을 돈 트윈의 사건들이 전부 `2026-01-01T00:00:xx` 에 몰려 있었고, 그래서 「언제 있었던 일인가」를
294
+ * 되짚을 수 없었다. 실 시각으로 창을 자르는 성과·이력 질의에는 그 트윈이 아예 보이지 않는다.
295
+ *
296
+ * 에너지에서는 더 아프다: 수요 구간은 벽시계의 15분에 맞춰 끊어야 요금과 견줄 수 있는데, 기준점이
297
+ * 가짜면 그 구간도 가짜 시각에 선다.
298
+ *
299
+ * 기본값은 그대로 둔다(시험·결정성). 살아 있는 시뮬 트윈을 세우는 호스트가 실제 시각으로 옮긴다.
300
+ */
301
+ protected originMs: number;
288
302
  protected rng: Rng;
289
303
  protected policy: AllocationPolicy;
290
304
  /** duration 시임(선택) — 미주입 시 명세, 명세도 없으면 도메인 상수. 이력 보정 추정기가 여기 들어온다. */
@@ -452,6 +466,15 @@ export declare abstract class FlowEngine implements TwinKernel {
452
466
  * 아직 아무것도 못 들었으면 시뮬 기준으로 떨어진다(그때는 판정할 사실도 없다).
453
467
  */
454
468
  protected nowMs(): number;
469
+ /**
470
+ * 시각 기준점을 세운다 — **살아 있는 시뮬 트윈은 실제 시각 위에서 돈다.**
471
+ *
472
+ * 세우는 쪽(호스트)이 기동 순간의 실제 시각을 준다. 재기동하면 그만큼 앞으로 뛰는데, 그것이 사실이다
473
+ * (그 사이 이 트윈은 돌지 않았고, 저널의 빈 구간이 그 사실을 말한다).
474
+ *
475
+ * 이미 시계가 흐른 뒤에 옮기면 그전에 낸 사실들과 시간축이 어긋나므로 **기동 직후에만** 부른다.
476
+ */
477
+ setClockOrigin(originMs: number): void;
455
478
  protected now(): string;
456
479
  /**
457
480
  * 자극이 선언한 **약속**을 오더 필드로 — 표준 `OperationsRequest.Priority`·`StartTime`·`EndTime`.
@@ -227,6 +227,20 @@ export class FlowEngine {
227
227
  orders = new Map();
228
228
  revision = 0;
229
229
  clockMs = 0;
230
+ /**
231
+ * 시각의 **기준점** — 이 트윈의 「지금」은 `originMs + clockMs` 다.
232
+ *
233
+ * ── 왜 고정 상수가 아닌가 (2026-08-15) ─────────────────────────────────────
234
+ * 기준점이 `BASE_EPOCH` 하나로 고정돼 있어서, 시뮬 트윈의 저널 시각이 **재기동마다 되감겼다.**
235
+ * 며칠을 돈 트윈의 사건들이 전부 `2026-01-01T00:00:xx` 에 몰려 있었고, 그래서 「언제 있었던 일인가」를
236
+ * 되짚을 수 없었다. 실 시각으로 창을 자르는 성과·이력 질의에는 그 트윈이 아예 보이지 않는다.
237
+ *
238
+ * 에너지에서는 더 아프다: 수요 구간은 벽시계의 15분에 맞춰 끊어야 요금과 견줄 수 있는데, 기준점이
239
+ * 가짜면 그 구간도 가짜 시각에 선다.
240
+ *
241
+ * 기본값은 그대로 둔다(시험·결정성). 살아 있는 시뮬 트윈을 세우는 호스트가 실제 시각으로 옮긴다.
242
+ */
243
+ originMs = BASE_EPOCH;
230
244
  rng = mulberry32(1);
231
245
  policy;
232
246
  /** duration 시임(선택) — 미주입 시 명세, 명세도 없으면 도메인 상수. 이력 보정 추정기가 여기 들어온다. */
@@ -1016,7 +1030,20 @@ export class FlowEngine {
1016
1030
  */
1017
1031
  nowMs() {
1018
1032
  const observed = this.observeMode ? this.observer?.lastObservedMs : undefined;
1019
- return observed ?? BASE_EPOCH + this.clockMs;
1033
+ return observed ?? this.originMs + this.clockMs;
1034
+ }
1035
+ /**
1036
+ * 시각 기준점을 세운다 — **살아 있는 시뮬 트윈은 실제 시각 위에서 돈다.**
1037
+ *
1038
+ * 세우는 쪽(호스트)이 기동 순간의 실제 시각을 준다. 재기동하면 그만큼 앞으로 뛰는데, 그것이 사실이다
1039
+ * (그 사이 이 트윈은 돌지 않았고, 저널의 빈 구간이 그 사실을 말한다).
1040
+ *
1041
+ * 이미 시계가 흐른 뒤에 옮기면 그전에 낸 사실들과 시간축이 어긋나므로 **기동 직후에만** 부른다.
1042
+ */
1043
+ setClockOrigin(originMs) {
1044
+ if (!Number.isFinite(originMs))
1045
+ throw new Error('clock origin must be a finite epoch millisecond value');
1046
+ this.originMs = originMs;
1020
1047
  }
1021
1048
  now() { return new Date(this.nowMs()).toISOString(); }
1022
1049
  /**
@@ -2809,6 +2809,20 @@ var FlowEngine = class {
2809
2809
  orders = /* @__PURE__ */ new Map();
2810
2810
  revision = 0;
2811
2811
  clockMs = 0;
2812
+ /**
2813
+ * 시각의 **기준점** — 이 트윈의 「지금」은 `originMs + clockMs` 다.
2814
+ *
2815
+ * ── 왜 고정 상수가 아닌가 (2026-08-15) ─────────────────────────────────────
2816
+ * 기준점이 `BASE_EPOCH` 하나로 고정돼 있어서, 시뮬 트윈의 저널 시각이 **재기동마다 되감겼다.**
2817
+ * 며칠을 돈 트윈의 사건들이 전부 `2026-01-01T00:00:xx` 에 몰려 있었고, 그래서 「언제 있었던 일인가」를
2818
+ * 되짚을 수 없었다. 실 시각으로 창을 자르는 성과·이력 질의에는 그 트윈이 아예 보이지 않는다.
2819
+ *
2820
+ * 에너지에서는 더 아프다: 수요 구간은 벽시계의 15분에 맞춰 끊어야 요금과 견줄 수 있는데, 기준점이
2821
+ * 가짜면 그 구간도 가짜 시각에 선다.
2822
+ *
2823
+ * 기본값은 그대로 둔다(시험·결정성). 살아 있는 시뮬 트윈을 세우는 호스트가 실제 시각으로 옮긴다.
2824
+ */
2825
+ originMs = BASE_EPOCH;
2812
2826
  rng = mulberry32(1);
2813
2827
  policy;
2814
2828
  /** duration 시임(선택) — 미주입 시 명세, 명세도 없으면 도메인 상수. 이력 보정 추정기가 여기 들어온다. */
@@ -3529,7 +3543,19 @@ var FlowEngine = class {
3529
3543
  */
3530
3544
  nowMs() {
3531
3545
  const observed = this.observeMode ? this.observer?.lastObservedMs : void 0;
3532
- return observed ?? BASE_EPOCH + this.clockMs;
3546
+ return observed ?? this.originMs + this.clockMs;
3547
+ }
3548
+ /**
3549
+ * 시각 기준점을 세운다 — **살아 있는 시뮬 트윈은 실제 시각 위에서 돈다.**
3550
+ *
3551
+ * 세우는 쪽(호스트)이 기동 순간의 실제 시각을 준다. 재기동하면 그만큼 앞으로 뛰는데, 그것이 사실이다
3552
+ * (그 사이 이 트윈은 돌지 않았고, 저널의 빈 구간이 그 사실을 말한다).
3553
+ *
3554
+ * 이미 시계가 흐른 뒤에 옮기면 그전에 낸 사실들과 시간축이 어긋나므로 **기동 직후에만** 부른다.
3555
+ */
3556
+ setClockOrigin(originMs) {
3557
+ if (!Number.isFinite(originMs)) throw new Error("clock origin must be a finite epoch millisecond value");
3558
+ this.originMs = originMs;
3533
3559
  }
3534
3560
  now() {
3535
3561
  return new Date(this.nowMs()).toISOString();
@@ -5802,11 +5828,22 @@ var EmsKernel = class extends FlowEngine {
5802
5828
  * kW 를 못 읽은 경우(계기 오류·필드 누락)가 실제로 있고, 그것은 「아무것도 오지 않았다」와
5803
5829
  * 다르다. 무엇이 왔는지는 지점별 `samplesInWindow` 가 답한다.
5804
5830
  */
5805
- ...w.samples === 0 ? { observedAbsence: "no-load-samples" } : {}
5831
+ ...w.samples === 0 ? { observedAbsence: "no-load-samples" } : {},
5832
+ /*
5833
+ * **만든 값이면 그렇게 말한다.** 상태에만 표시하고 사실에는 빠뜨리면, 저널을 읽는 쪽
5834
+ * (성과·이력·보고서)이 시뮬레이션의 수를 계측으로 읽는다 — 값이 그럴듯할수록 위험하다.
5835
+ */
5836
+ ...w.derived ? { derived: true } : {}
5806
5837
  });
5807
5838
  if (w.maxKW !== void 0 && (this.peak === void 0 || w.maxKW > this.peak.kW)) {
5808
5839
  this.peak = { kW: w.maxKW, windowStartMs: w.startMs };
5809
- this.emitOp(ENERGY_EVENT.peak, { kW: w.maxKW, windowStartMs: w.startMs, ...w.contractKW !== void 0 ? { contractKW: w.contractKW } : {} });
5840
+ this.emitOp(ENERGY_EVENT.peak, {
5841
+ kW: w.maxKW,
5842
+ windowStartMs: w.startMs,
5843
+ ...w.contractKW !== void 0 ? { contractKW: w.contractKW } : {},
5844
+ /* 피크도 그 구간에서 왔다 — 구간이 만든 값이면 피크도 만든 값이다. */
5845
+ ...w.derived ? { derived: true } : {}
5846
+ });
5810
5847
  }
5811
5848
  this.open = void 0;
5812
5849
  }
@@ -5926,8 +5963,9 @@ var EmsKernel = class extends FlowEngine {
5926
5963
  }
5927
5964
  tick(dtMs) {
5928
5965
  super.tick(dtMs);
5929
- this.deriveLoad(this.clockMs);
5930
- this.closeDue(this.clockMs);
5966
+ const at = this.nowMs();
5967
+ this.deriveLoad(at);
5968
+ this.closeDue(at);
5931
5969
  }
5932
5970
  getSnapshot() {
5933
5971
  const snap = super.getSnapshot();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/twin-kernel",
3
- "version": "0.7.4",
3
+ "version": "0.7.5",
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": {