@operato/twin-kernel 0.7.87 → 0.7.88

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.
@@ -2041,6 +2041,19 @@ export interface EnergyState {
2041
2041
  * 청구서보다 이쪽이 먼저다: 청구서는 끝난 기간의 정산이고 이것은 지금 적용되는 값이다.
2042
2042
  */
2043
2043
  tariffBasis?: EnergyTariffBasisData;
2044
+ /**
2045
+ * **선언된 요금 기준들** — 유효 구간별. `tariffBasis` 는 이 중 지금 유효한 것이다.
2046
+ *
2047
+ * ── 왜 여럿을 드나 (2026-08-30) ──────────────────────────────────────────
2048
+ * 선언은 **예정될 수 있다.** 한전은 다음 달 요금을 미리 공표한다. 하나만 들면 9월 선언이 들어오는
2049
+ * 순간 8월 선언을 덮어써서 **오늘 요금이 다음 달 단가로 계산된다.** 오류는 나지 않는다.
2050
+ *
2051
+ * 처음에는 예정된 선언을 유입에서 거부해 이 상황을 막았다. 그것은 정상적인 사실을 막는 것이라,
2052
+ * 담을 자리를 만들고 거부를 풀었다.
2053
+ *
2054
+ * 고르는 규칙은 **커널에 한 벌만 둔다.** 읽는 쪽이 각자 고르면 화면마다 다른 요금이 나온다.
2055
+ */
2056
+ tariffBasisSchedule?: EnergyTariffBasisData[];
2044
2057
  /**
2045
2058
  * 이 트윈의 **발전 정격**과 그 근거 — 이용률의 분모(§ `generationRated`).
2046
2059
  *
@@ -2056,8 +2069,12 @@ export interface EnergyState {
2056
2069
  * 마지막으로 받은 **발전 단가** — 이 기간에 낸 전기 1kWh 의 값.
2057
2070
  *
2058
2071
  * 창 밖의 단가도 이 창에 적용될 수 있어(단가는 기간마다 정해진다) 상태로 이어 간다.
2072
+ *
2073
+ * 예정된 단가는 `generationPriceSchedule` 에 함께 든다 — 이 칸은 **지금 유효한 것**이다.
2059
2074
  */
2060
2075
  generationPrice?: EnergyGenerationPriceData;
2076
+ /** **선언된 발전 단가들** — 유효 구간별. 예정된 것을 담는다(§`tariffBasisSchedule`). */
2077
+ generationPriceSchedule?: EnergyGenerationPriceData[];
2061
2078
  /**
2062
2079
  * 이 청구 주기의 **최고 수요** — 기본요금이 이 값으로 다시 정해진다.
2063
2080
  *
@@ -133,6 +133,13 @@ export declare class EmsKernel extends FlowEngine {
133
133
  * 새것을 쓴다 — 사업자가 그 주기의 기준을 고칠 수 있다.
134
134
  */
135
135
  private applyTariffBasis;
136
+ /**
137
+ * 시계가 선언의 경계를 넘으면 **지금 유효한 선언이 바뀐다.**
138
+ *
139
+ * 표본이 들어올 때만 다시 고르면, 9월 1일 0시에 단가가 바뀌었는데 다음 표본이 올 때까지 8월 단가로
140
+ * 계산한다. 태양광은 밤에 표본이 없어 그 간격이 길다.
141
+ */
142
+ protected refreshDeclarationsInEffect(nowMs: number): void;
136
143
  /**
137
144
  * 청구서를 상태에 둔다 — **우리 계산을 덮지 않는다.**
138
145
  *
@@ -221,8 +228,12 @@ export declare class EmsKernel extends FlowEngine {
221
228
  private lastBill?;
222
229
  /** 마지막으로 받은 요금 기준 — 이 주기에 적용되는 요금적용전력과 단가. */
223
230
  private tariffBasis?;
231
+ /** 선언된 요금 기준들 — 예정된 것을 포함한다(§`EnergyState.tariffBasisSchedule`). */
232
+ private tariffBasisSchedule?;
224
233
  /** 마지막으로 받은 발전 단가 — 이 기간에 낸 전기 1kWh 의 값. */
225
234
  private generationPrice?;
235
+ /** 선언된 발전 단가들 — 예정된 것을 포함한다. */
236
+ private generationPriceSchedule?;
226
237
  /**
227
238
  * 이 청구 주기의 **최고 수요** — 기본요금이 이 값으로 다시 정해진다.
228
239
  *
@@ -354,13 +354,8 @@ export class EmsKernel extends FlowEngine {
354
354
  const d = envelope.data;
355
355
  if (!d?.from || !d?.to)
356
356
  return;
357
- const fromMs = Date.parse(String(d.from));
358
- if (!Number.isFinite(fromMs))
359
- return;
360
- const priorFromMs = Date.parse(String(this.generationPrice?.from ?? ''));
361
- if (Number.isFinite(priorFromMs) && fromMs < priorFromMs)
362
- return;
363
- this.generationPrice = { ...d };
357
+ this.generationPriceSchedule = putDeclaration(this.generationPriceSchedule, d);
358
+ this.generationPrice = inEffectAt(this.generationPriceSchedule, this.nowMs());
364
359
  this.revision++;
365
360
  }
366
361
  /**
@@ -373,15 +368,25 @@ export class EmsKernel extends FlowEngine {
373
368
  const d = envelope.data;
374
369
  if (!d?.from || !d?.to)
375
370
  return;
376
- const fromMs = Date.parse(String(d.from));
377
- if (!Number.isFinite(fromMs))
378
- return;
379
- const priorFromMs = Date.parse(String(this.tariffBasis?.from ?? ''));
380
- if (Number.isFinite(priorFromMs) && fromMs < priorFromMs)
381
- return;
382
- this.tariffBasis = { ...d };
371
+ this.tariffBasisSchedule = putDeclaration(this.tariffBasisSchedule, d);
372
+ this.tariffBasis = inEffectAt(this.tariffBasisSchedule, this.nowMs());
383
373
  this.revision++;
384
374
  }
375
+ /**
376
+ * 시계가 선언의 경계를 넘으면 **지금 유효한 선언이 바뀐다.**
377
+ *
378
+ * 표본이 들어올 때만 다시 고르면, 9월 1일 0시에 단가가 바뀌었는데 다음 표본이 올 때까지 8월 단가로
379
+ * 계산한다. 태양광은 밤에 표본이 없어 그 간격이 길다.
380
+ */
381
+ refreshDeclarationsInEffect(nowMs) {
382
+ const tariff = inEffectAt(this.tariffBasisSchedule, nowMs);
383
+ const price = inEffectAt(this.generationPriceSchedule, nowMs);
384
+ if (tariff !== this.tariffBasis || price !== this.generationPrice) {
385
+ this.tariffBasis = tariff;
386
+ this.generationPrice = price;
387
+ this.revision++;
388
+ }
389
+ }
385
390
  /**
386
391
  * 청구서를 상태에 둔다 — **우리 계산을 덮지 않는다.**
387
392
  *
@@ -713,8 +718,12 @@ export class EmsKernel extends FlowEngine {
713
718
  lastBill;
714
719
  /** 마지막으로 받은 요금 기준 — 이 주기에 적용되는 요금적용전력과 단가. */
715
720
  tariffBasis;
721
+ /** 선언된 요금 기준들 — 예정된 것을 포함한다(§`EnergyState.tariffBasisSchedule`). */
722
+ tariffBasisSchedule;
716
723
  /** 마지막으로 받은 발전 단가 — 이 기간에 낸 전기 1kWh 의 값. */
717
724
  generationPrice;
725
+ /** 선언된 발전 단가들 — 예정된 것을 포함한다. */
726
+ generationPriceSchedule;
718
727
  /**
719
728
  * 이 청구 주기의 **최고 수요** — 기본요금이 이 값으로 다시 정해진다.
720
729
  *
@@ -1599,6 +1608,8 @@ export class EmsKernel extends FlowEngine {
1599
1608
  * 계량은 이미 시계로 마감한다(`closeDue`). 한 커널 안에서 규칙이 두 벌일 이유가 없다.
1600
1609
  */
1601
1610
  this.closeGenerationPeriodsDue(at);
1611
+ /* 시계가 선언의 경계를 넘으면 지금 유효한 선언이 바뀐다 — 표본을 기다리지 않는다. */
1612
+ this.refreshDeclarationsInEffect(at);
1602
1613
  /* 틱 길이를 함께 넘긴다 — 방전량을 에너지로 바꿀 때 필요하다(필드로 숨기면 어디서 온 값인지 흐려진다). */
1603
1614
  this.deriveLoad(at, dtMs);
1604
1615
  this.closeDue(at);
@@ -1657,10 +1668,21 @@ export class EmsKernel extends FlowEngine {
1657
1668
  /* 청구서도 이어받는다 — 재기동마다 사라지면 우리 계산과 견줄 기준이 없어진다. */
1658
1669
  if (e.lastBill?.from && e.lastBill?.to)
1659
1670
  this.lastBill = { ...e.lastBill };
1671
+ /*
1672
+ * 선언은 **목록째** 이어받는다 — 예정된 것이 사라지면 재기동 뒤에 시행일이 와도 옛 단가를 쓴다.
1673
+ * 이어받은 뒤 지금 시각으로 다시 고른다: 멈춰 있는 동안 시행일이 지났을 수 있다.
1674
+ */
1675
+ if (Array.isArray(e.tariffBasisSchedule))
1676
+ this.tariffBasisSchedule = e.tariffBasisSchedule.map(d => ({ ...d }));
1677
+ if (Array.isArray(e.generationPriceSchedule))
1678
+ this.generationPriceSchedule = e.generationPriceSchedule.map(d => ({ ...d }));
1660
1679
  if (e.tariffBasis?.from && e.tariffBasis?.to)
1661
1680
  this.tariffBasis = { ...e.tariffBasis };
1662
1681
  if (e.generationPrice?.from && e.generationPrice?.to)
1663
1682
  this.generationPrice = { ...e.generationPrice };
1683
+ if (this.tariffBasisSchedule?.length || this.generationPriceSchedule?.length) {
1684
+ this.refreshDeclarationsInEffect(this.nowMs());
1685
+ }
1664
1686
  /* 이 주기의 최고도 이어받는다 — 재기동마다 0 으로 돌아가면 이미 넘은 것을 못 넘었다고 말한다. */
1665
1687
  if (Number.isFinite(Number(e.demandHigh?.kW)) && e.demandHigh?.since)
1666
1688
  this.demandHigh = { ...e.demandHigh };
@@ -1691,6 +1713,14 @@ export class EmsKernel extends FlowEngine {
1691
1713
  ...(this.lastBill ? { lastBill: { ...this.lastBill } } : {}),
1692
1714
  ...(this.tariffBasis ? { tariffBasis: { ...this.tariffBasis } } : {}),
1693
1715
  ...(this.generationPrice ? { generationPrice: { ...this.generationPrice } } : {}),
1716
+ /*
1717
+ * **예정된 선언도 함께 낸다.** 지금 유효한 것만 이어받으면, 재기동한 뒤 9월이 되어도 8월 단가로
1718
+ * 계산한다 — 9월 선언이 어디에도 남아 있지 않기 때문이다. 오늘 이 부류를 세 번 겪었다.
1719
+ */
1720
+ ...(this.tariffBasisSchedule?.length ? { tariffBasisSchedule: this.tariffBasisSchedule.map(d => ({ ...d })) } : {}),
1721
+ ...(this.generationPriceSchedule?.length
1722
+ ? { generationPriceSchedule: this.generationPriceSchedule.map(d => ({ ...d })) }
1723
+ : {}),
1694
1724
  ...(() => {
1695
1725
  const r = this.generationRated();
1696
1726
  return r ? { generationRated: r } : {};
@@ -1705,3 +1735,41 @@ export class EmsKernel extends FlowEngine {
1705
1735
  return { ...snap, energy };
1706
1736
  }
1707
1737
  }
1738
+ /**
1739
+ * 선언 하나를 목록에 넣는다 — **같은 유효 구간이면 새것이 이긴다.**
1740
+ *
1741
+ * 사업자가 그 주기의 기준을 고칠 수 있다. 같은 구간에 두 선언이 남으면 어느 것이 맞는지 말할 수 없다.
1742
+ *
1743
+ * 목록은 시작 시각 순으로 두고 **24개까지** 든다. 선언은 주기마다 하나이므로 24개면 두 해치다 —
1744
+ * 그보다 오래된 것은 지난 기록에서 읽는다(상태는 「지금」을 위한 것이다).
1745
+ */
1746
+ function putDeclaration(list, d) {
1747
+ const rest = (list ?? []).filter(x => !(x.from === d.from && x.to === d.to));
1748
+ const next = [...rest, { ...d }].sort((a, b) => Date.parse(a.from) - Date.parse(b.from));
1749
+ return next.length > 24 ? next.slice(next.length - 24) : next;
1750
+ }
1751
+ /**
1752
+ * **이 시각에 유효한 선언** — 시작했고 아직 끝나지 않은 것.
1753
+ *
1754
+ * 유효한 것이 없으면 답하지 않는다. 가장 가까운 것을 골라 주지 않는다 — 아직 시작하지 않은 단가로
1755
+ * 오늘을 계산하거나, 이미 끝난 단가를 지금 것인 양 쓰게 된다. 둘 다 오류 없이 틀린 값을 만든다.
1756
+ *
1757
+ * 구간이 겹치면 **나중에 시작한 것**이 이긴다(더 좁게 정한 선언이 앞선다).
1758
+ */
1759
+ function inEffectAt(list, atMs) {
1760
+ let best;
1761
+ let bestFrom = -Infinity;
1762
+ for (const d of list ?? []) {
1763
+ const from = Date.parse(String(d.from));
1764
+ const to = Date.parse(String(d.to));
1765
+ if (!Number.isFinite(from) || !Number.isFinite(to))
1766
+ continue;
1767
+ if (atMs < from || atMs >= to)
1768
+ continue;
1769
+ if (from > bestFrom) {
1770
+ best = d;
1771
+ bestFrom = from;
1772
+ }
1773
+ }
1774
+ return best;
1775
+ }
@@ -503,11 +503,6 @@ export function ingestEnergyGenerationPriceRecords(records, opts) {
503
503
  continue;
504
504
  }
505
505
  const data = { from: span.from, to: span.to, unitPrice, currency };
506
- const futureError = futureFactError(span.from, opts);
507
- if (futureError) {
508
- rejected.push({ record: r, errors: [futureError] });
509
- continue;
510
- }
511
506
  accepted.push({
512
507
  eventId: periodFactId(opts.tenantId, ENERGY_EVENT.generationPrice, '', span.from, span.to),
513
508
  eventType: ENERGY_EVENT.generationPrice,
@@ -573,11 +568,6 @@ export function ingestEnergyTariffBasisRecords(records, opts) {
573
568
  ...(demandChargePerKW !== undefined ? { demandChargePerKW } : {}),
574
569
  ...(currency ? { currency } : {})
575
570
  };
576
- const futureError = futureFactError(span.from, opts);
577
- if (futureError) {
578
- rejected.push({ record: r, errors: [futureError] });
579
- continue;
580
- }
581
571
  accepted.push({
582
572
  eventId: periodFactId(opts.tenantId, ENERGY_EVENT.tariffBasis, '', span.from, span.to),
583
573
  eventType: ENERGY_EVENT.tariffBasis,
@@ -633,8 +623,13 @@ export function isEnergyBillRecord(record) {
633
623
  * 지난 기록 채우기는 이미 이 판단을 하고 있었다(§`judgeBackfill`). 라이브 문에만 없어서 **한 종류에
634
624
  * 규칙이 두 벌**이었다. 같은 사실이 어느 문으로 들어오느냐에 따라 받아지기도 하고 거절되기도 했다.
635
625
  *
636
- * 기준 선언(요금 기준·발전 단가)은 기간이 미래로 뻗어도 된다 — 그쪽은 사건 시각이 **기간의 시작**이라
637
- * 이 판정에 걸리지 않는다. 앞으로 이렇게 매기겠다는 선언은 오늘 성립한다.
626
+ * ── 기준 선언은 이 판정을 받지 않는다 (2026-08-30, 두 번째 판단) ────────────
627
+ * 요금 기준·발전 단가는 **예정될 수 있다.** 한전은 다음 달 요금을 미리 공표하고, 공정 조건도 시행일을
628
+ * 앞두고 정해진다. 「9월부터 이 단가」는 오늘 성립하는 사실이다.
629
+ *
630
+ * 처음에는 이 둘도 거부했다. 그러면 정상적인 사실이 막힌다. 받되, **예정된 선언이 지금 것을 덮지 않게**
631
+ * 커널이 유효 구간으로 고른다(§`EnergyState.tariffBasisSchedule`). 받아 놓고 아무 데나 쓰면
632
+ * 오늘 요금이 다음 달 단가로 계산된다 — 그것이 이 문을 열기 전에 먼저 만들어야 했던 자리다.
638
633
  */
639
634
  function futureFactError(eventTime, opts) {
640
635
  const nowText = opts.nowISO ?? opts.defaultEventTime;
@@ -2862,11 +2862,6 @@ function ingestEnergyGenerationPriceRecords(records, opts) {
2862
2862
  continue;
2863
2863
  }
2864
2864
  const data = { from: span.from, to: span.to, unitPrice, currency };
2865
- const futureError = futureFactError(span.from, opts);
2866
- if (futureError) {
2867
- rejected.push({ record: r, errors: [futureError] });
2868
- continue;
2869
- }
2870
2865
  accepted.push({
2871
2866
  eventId: periodFactId(opts.tenantId, ENERGY_EVENT.generationPrice, "", span.from, span.to),
2872
2867
  eventType: ENERGY_EVENT.generationPrice,
@@ -2912,11 +2907,6 @@ function ingestEnergyTariffBasisRecords(records, opts) {
2912
2907
  ...demandChargePerKW !== void 0 ? { demandChargePerKW } : {},
2913
2908
  ...currency ? { currency } : {}
2914
2909
  };
2915
- const futureError = futureFactError(span.from, opts);
2916
- if (futureError) {
2917
- rejected.push({ record: r, errors: [futureError] });
2918
- continue;
2919
- }
2920
2910
  accepted.push({
2921
2911
  eventId: periodFactId(opts.tenantId, ENERGY_EVENT.tariffBasis, "", span.from, span.to),
2922
2912
  eventType: ENERGY_EVENT.tariffBasis,
@@ -8673,11 +8663,8 @@ var EmsKernel = class extends FlowEngine {
8673
8663
  applyGenerationPrice(envelope) {
8674
8664
  const d = envelope.data;
8675
8665
  if (!d?.from || !d?.to) return;
8676
- const fromMs = Date.parse(String(d.from));
8677
- if (!Number.isFinite(fromMs)) return;
8678
- const priorFromMs = Date.parse(String(this.generationPrice?.from ?? ""));
8679
- if (Number.isFinite(priorFromMs) && fromMs < priorFromMs) return;
8680
- this.generationPrice = { ...d };
8666
+ this.generationPriceSchedule = putDeclaration(this.generationPriceSchedule, d);
8667
+ this.generationPrice = inEffectAt(this.generationPriceSchedule, this.nowMs());
8681
8668
  this.revision++;
8682
8669
  }
8683
8670
  /**
@@ -8689,13 +8676,25 @@ var EmsKernel = class extends FlowEngine {
8689
8676
  applyTariffBasis(envelope) {
8690
8677
  const d = envelope.data;
8691
8678
  if (!d?.from || !d?.to) return;
8692
- const fromMs = Date.parse(String(d.from));
8693
- if (!Number.isFinite(fromMs)) return;
8694
- const priorFromMs = Date.parse(String(this.tariffBasis?.from ?? ""));
8695
- if (Number.isFinite(priorFromMs) && fromMs < priorFromMs) return;
8696
- this.tariffBasis = { ...d };
8679
+ this.tariffBasisSchedule = putDeclaration(this.tariffBasisSchedule, d);
8680
+ this.tariffBasis = inEffectAt(this.tariffBasisSchedule, this.nowMs());
8697
8681
  this.revision++;
8698
8682
  }
8683
+ /**
8684
+ * 시계가 선언의 경계를 넘으면 **지금 유효한 선언이 바뀐다.**
8685
+ *
8686
+ * 표본이 들어올 때만 다시 고르면, 9월 1일 0시에 단가가 바뀌었는데 다음 표본이 올 때까지 8월 단가로
8687
+ * 계산한다. 태양광은 밤에 표본이 없어 그 간격이 길다.
8688
+ */
8689
+ refreshDeclarationsInEffect(nowMs) {
8690
+ const tariff = inEffectAt(this.tariffBasisSchedule, nowMs);
8691
+ const price = inEffectAt(this.generationPriceSchedule, nowMs);
8692
+ if (tariff !== this.tariffBasis || price !== this.generationPrice) {
8693
+ this.tariffBasis = tariff;
8694
+ this.generationPrice = price;
8695
+ this.revision++;
8696
+ }
8697
+ }
8699
8698
  /**
8700
8699
  * 청구서를 상태에 둔다 — **우리 계산을 덮지 않는다.**
8701
8700
  *
@@ -8914,8 +8913,12 @@ var EmsKernel = class extends FlowEngine {
8914
8913
  lastBill;
8915
8914
  /** 마지막으로 받은 요금 기준 — 이 주기에 적용되는 요금적용전력과 단가. */
8916
8915
  tariffBasis;
8916
+ /** 선언된 요금 기준들 — 예정된 것을 포함한다(§`EnergyState.tariffBasisSchedule`). */
8917
+ tariffBasisSchedule;
8917
8918
  /** 마지막으로 받은 발전 단가 — 이 기간에 낸 전기 1kWh 의 값. */
8918
8919
  generationPrice;
8920
+ /** 선언된 발전 단가들 — 예정된 것을 포함한다. */
8921
+ generationPriceSchedule;
8919
8922
  /**
8920
8923
  * 이 청구 주기의 **최고 수요** — 기본요금이 이 값으로 다시 정해진다.
8921
8924
  *
@@ -9552,6 +9555,7 @@ var EmsKernel = class extends FlowEngine {
9552
9555
  super.tick(dtMs);
9553
9556
  const at = this.nowMs();
9554
9557
  this.closeGenerationPeriodsDue(at);
9558
+ this.refreshDeclarationsInEffect(at);
9555
9559
  this.deriveLoad(at, dtMs);
9556
9560
  this.closeDue(at);
9557
9561
  }
@@ -9591,8 +9595,13 @@ var EmsKernel = class extends FlowEngine {
9591
9595
  }
9592
9596
  if (Number.isFinite(e.unclosedGenerationPeriods)) this.unclosedGenerationPeriods = Number(e.unclosedGenerationPeriods);
9593
9597
  if (e.lastBill?.from && e.lastBill?.to) this.lastBill = { ...e.lastBill };
9598
+ if (Array.isArray(e.tariffBasisSchedule)) this.tariffBasisSchedule = e.tariffBasisSchedule.map((d) => ({ ...d }));
9599
+ if (Array.isArray(e.generationPriceSchedule)) this.generationPriceSchedule = e.generationPriceSchedule.map((d) => ({ ...d }));
9594
9600
  if (e.tariffBasis?.from && e.tariffBasis?.to) this.tariffBasis = { ...e.tariffBasis };
9595
9601
  if (e.generationPrice?.from && e.generationPrice?.to) this.generationPrice = { ...e.generationPrice };
9602
+ if (this.tariffBasisSchedule?.length || this.generationPriceSchedule?.length) {
9603
+ this.refreshDeclarationsInEffect(this.nowMs());
9604
+ }
9596
9605
  if (Number.isFinite(Number(e.demandHigh?.kW)) && e.demandHigh?.since) this.demandHigh = { ...e.demandHigh };
9597
9606
  }
9598
9607
  getSnapshot() {
@@ -9617,6 +9626,12 @@ var EmsKernel = class extends FlowEngine {
9617
9626
  ...this.lastBill ? { lastBill: { ...this.lastBill } } : {},
9618
9627
  ...this.tariffBasis ? { tariffBasis: { ...this.tariffBasis } } : {},
9619
9628
  ...this.generationPrice ? { generationPrice: { ...this.generationPrice } } : {},
9629
+ /*
9630
+ * **예정된 선언도 함께 낸다.** 지금 유효한 것만 이어받으면, 재기동한 뒤 9월이 되어도 8월 단가로
9631
+ * 계산한다 — 9월 선언이 어디에도 남아 있지 않기 때문이다. 오늘 이 부류를 세 번 겪었다.
9632
+ */
9633
+ ...this.tariffBasisSchedule?.length ? { tariffBasisSchedule: this.tariffBasisSchedule.map((d) => ({ ...d })) } : {},
9634
+ ...this.generationPriceSchedule?.length ? { generationPriceSchedule: this.generationPriceSchedule.map((d) => ({ ...d })) } : {},
9620
9635
  ...(() => {
9621
9636
  const r = this.generationRated();
9622
9637
  return r ? { generationRated: r } : {};
@@ -9629,6 +9644,26 @@ var EmsKernel = class extends FlowEngine {
9629
9644
  return { ...snap, energy };
9630
9645
  }
9631
9646
  };
9647
+ function putDeclaration(list, d) {
9648
+ const rest = (list ?? []).filter((x) => !(x.from === d.from && x.to === d.to));
9649
+ const next = [...rest, { ...d }].sort((a, b) => Date.parse(a.from) - Date.parse(b.from));
9650
+ return next.length > 24 ? next.slice(next.length - 24) : next;
9651
+ }
9652
+ function inEffectAt(list, atMs) {
9653
+ let best;
9654
+ let bestFrom = -Infinity;
9655
+ for (const d of list ?? []) {
9656
+ const from = Date.parse(String(d.from));
9657
+ const to = Date.parse(String(d.to));
9658
+ if (!Number.isFinite(from) || !Number.isFinite(to)) continue;
9659
+ if (atMs < from || atMs >= to) continue;
9660
+ if (from > bestFrom) {
9661
+ best = d;
9662
+ bestFrom = from;
9663
+ }
9664
+ }
9665
+ return best;
9666
+ }
9632
9667
 
9633
9668
  // src/event-journal.ts
9634
9669
  var DOMAIN_KERNELS = { wms: WmsKernel, yms: YmsKernel, mes: MesKernel, ems: EmsKernel };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/twin-kernel",
3
- "version": "0.7.87",
3
+ "version": "0.7.88",
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": {