@operato/twin-kernel 0.7.80 → 0.7.81

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.
@@ -399,7 +399,6 @@ export function ingestEnergyGenerationPriceRecords(records, opts) {
399
399
  const list = records === undefined || records === null ? [] : Array.isArray(records) ? records : [records];
400
400
  const accepted = [];
401
401
  const rejected = [];
402
- let seq = 0;
403
402
  for (const r of list) {
404
403
  const errors = [];
405
404
  const span = readSpan(r, errors);
@@ -416,7 +415,7 @@ export function ingestEnergyGenerationPriceRecords(records, opts) {
416
415
  }
417
416
  const data = { from: span.from, to: span.to, unitPrice, currency };
418
417
  accepted.push({
419
- eventId: `${opts.tenantId}-generation-price-${++seq}`,
418
+ eventId: periodFactId(opts.tenantId, ENERGY_EVENT.generationPrice, '', span.from, span.to),
420
419
  eventType: ENERGY_EVENT.generationPrice,
421
420
  eventTime: span.from,
422
421
  tenantId: opts.tenantId,
@@ -453,7 +452,6 @@ export function ingestEnergyTariffBasisRecords(records, opts) {
453
452
  const list = records === undefined || records === null ? [] : Array.isArray(records) ? records : [records];
454
453
  const accepted = [];
455
454
  const rejected = [];
456
- let seq = 0;
457
455
  for (const r of list) {
458
456
  const errors = [];
459
457
  const span = readSpan(r, errors);
@@ -482,7 +480,7 @@ export function ingestEnergyTariffBasisRecords(records, opts) {
482
480
  ...(currency ? { currency } : {})
483
481
  };
484
482
  accepted.push({
485
- eventId: `${opts.tenantId}-tariff-basis-${++seq}`,
483
+ eventId: periodFactId(opts.tenantId, ENERGY_EVENT.tariffBasis, '', span.from, span.to),
486
484
  eventType: ENERGY_EVENT.tariffBasis,
487
485
  /* **시작 시각**이다 — 그때부터 유효하다(§ 위 주석). */
488
486
  eventTime: span.from,
@@ -500,6 +498,27 @@ export function isEnergyBillRecord(record) {
500
498
  return false;
501
499
  return r.energyCharge !== undefined || r.demandCharge !== undefined || r.total !== undefined;
502
500
  }
501
+ /*
502
+ * ── 마감된 구간 사실의 **정체성** (2026-08-30) ───────────────────────────────
503
+ *
504
+ * 커널이 다루는 사실에는 두 종류가 있다.
505
+ *
506
+ * 시점의 관측 「이 시각에 이 값이었다」 같은 값이 두 번 오면 **두 사실**이다
507
+ * 마감된 구간 「이 기간에 이만큼이었다」 같은 기간이 두 번 오면 **한 사실**이다
508
+ *
509
+ * 둘째 종류는 정체성이 내용에 있다 — **종류·대상·시작·끝**이 같으면 같은 사실이다. 어느 커넥터가
510
+ * 어떻게 보내든, 재기동을 몇 번 하든 그렇다.
511
+ *
512
+ * 그래서 이 문들의 봉투 id 를 **순번이 아니라 내용에서** 만든다. 순번으로 만들면 같은 기간을 다시
513
+ * 보낼 때마다 다른 id 가 나오고, 읽는 쪽이 같은 사실인지 알 방법이 없다.
514
+ *
515
+ * **이것만으로 중복이 막히지는 않는다.** 저널은 아직 이 id 를 유일성으로 쓰지 않는다(그 판단은
516
+ * 호스트의 것이고 드라이버마다 다르다). 여기서 하는 일은 **막을 근거를 만드는 것**이다.
517
+ */
518
+ function periodFactId(tenantId, kind, subject, from, to) {
519
+ /* 대상이 없는 사실(요금 기준·청구서·발전 단가)은 그 자리를 비운다 — 없는 것을 지어내지 않는다. */
520
+ return [tenantId, kind, subject, from, to].join('|');
521
+ }
503
522
  /** 두 시각을 읽고 순서를 본다 — 뒤집힌 구간은 받지 않는다(합이 음수가 된다). */
504
523
  function readSpan(r, errors) {
505
524
  const from = String(r?.from ?? '').trim();
@@ -534,7 +553,6 @@ export function ingestEnergyUsagePeriodRecords(records, opts) {
534
553
  const list = records === undefined || records === null ? [] : Array.isArray(records) ? records : [records];
535
554
  const accepted = [];
536
555
  const rejected = [];
537
- let seq = 0;
538
556
  for (const r of list) {
539
557
  const errors = [];
540
558
  const meterId = String(r?.meterId ?? '').trim();
@@ -581,7 +599,7 @@ export function ingestEnergyUsagePeriodRecords(records, opts) {
581
599
  ...(basis ? { basis } : {})
582
600
  };
583
601
  accepted.push({
584
- eventId: `${opts.tenantId}-usage-period-${++seq}`,
602
+ eventId: periodFactId(opts.tenantId, ENERGY_EVENT.usagePeriod, meterId, span.from, span.to),
585
603
  eventType: ENERGY_EVENT.usagePeriod,
586
604
  /* 구간의 **끝**이 이 사실이 성립한 시각이다 — 시작으로 달면 저널이 그 구간을 미리 안 것이 된다. */
587
605
  eventTime: span.to,
@@ -595,7 +613,6 @@ export function ingestEnergyBillRecords(records, opts) {
595
613
  const list = records === undefined || records === null ? [] : Array.isArray(records) ? records : [records];
596
614
  const accepted = [];
597
615
  const rejected = [];
598
- let seq = 0;
599
616
  for (const r of list) {
600
617
  const errors = [];
601
618
  const span = readSpan(r, errors);
@@ -628,7 +645,7 @@ export function ingestEnergyBillRecords(records, opts) {
628
645
  ...(billingDemandKW !== undefined ? { billingDemandKW } : {})
629
646
  };
630
647
  accepted.push({
631
- eventId: `${opts.tenantId}-bill-${++seq}`,
648
+ eventId: periodFactId(opts.tenantId, ENERGY_EVENT.bill, '', span.from, span.to),
632
649
  eventType: ENERGY_EVENT.bill,
633
650
  eventTime: span.to,
634
651
  tenantId: opts.tenantId,
@@ -10049,7 +10049,6 @@ function ingestEnergyGenerationPriceRecords(records, opts) {
10049
10049
  const list = records === void 0 || records === null ? [] : Array.isArray(records) ? records : [records];
10050
10050
  const accepted = [];
10051
10051
  const rejected = [];
10052
- let seq = 0;
10053
10052
  for (const r of list) {
10054
10053
  const errors = [];
10055
10054
  const span = readSpan(r, errors);
@@ -10063,7 +10062,7 @@ function ingestEnergyGenerationPriceRecords(records, opts) {
10063
10062
  }
10064
10063
  const data = { from: span.from, to: span.to, unitPrice, currency };
10065
10064
  accepted.push({
10066
- eventId: `${opts.tenantId}-generation-price-${++seq}`,
10065
+ eventId: periodFactId(opts.tenantId, ENERGY_EVENT.generationPrice, "", span.from, span.to),
10067
10066
  eventType: ENERGY_EVENT.generationPrice,
10068
10067
  eventTime: span.from,
10069
10068
  tenantId: opts.tenantId,
@@ -10083,7 +10082,6 @@ function ingestEnergyTariffBasisRecords(records, opts) {
10083
10082
  const list = records === void 0 || records === null ? [] : Array.isArray(records) ? records : [records];
10084
10083
  const accepted = [];
10085
10084
  const rejected = [];
10086
- let seq = 0;
10087
10085
  for (const r of list) {
10088
10086
  const errors = [];
10089
10087
  const span = readSpan(r, errors);
@@ -10109,7 +10107,7 @@ function ingestEnergyTariffBasisRecords(records, opts) {
10109
10107
  ...currency ? { currency } : {}
10110
10108
  };
10111
10109
  accepted.push({
10112
- eventId: `${opts.tenantId}-tariff-basis-${++seq}`,
10110
+ eventId: periodFactId(opts.tenantId, ENERGY_EVENT.tariffBasis, "", span.from, span.to),
10113
10111
  eventType: ENERGY_EVENT.tariffBasis,
10114
10112
  /* **시작 시각**이다 — 그때부터 유효하다(§ 위 주석). */
10115
10113
  eventTime: span.from,
@@ -10125,6 +10123,9 @@ function isEnergyBillRecord(record) {
10125
10123
  if (r.from === void 0 || r.to === void 0) return false;
10126
10124
  return r.energyCharge !== void 0 || r.demandCharge !== void 0 || r.total !== void 0;
10127
10125
  }
10126
+ function periodFactId(tenantId, kind, subject, from, to) {
10127
+ return [tenantId, kind, subject, from, to].join("|");
10128
+ }
10128
10129
  function readSpan(r, errors) {
10129
10130
  const from = String(r?.from ?? "").trim();
10130
10131
  const to = String(r?.to ?? "").trim();
@@ -10154,7 +10155,6 @@ function ingestEnergyUsagePeriodRecords(records, opts) {
10154
10155
  const list = records === void 0 || records === null ? [] : Array.isArray(records) ? records : [records];
10155
10156
  const accepted = [];
10156
10157
  const rejected = [];
10157
- let seq = 0;
10158
10158
  for (const r of list) {
10159
10159
  const errors = [];
10160
10160
  const meterId = String(r?.meterId ?? "").trim();
@@ -10194,7 +10194,7 @@ function ingestEnergyUsagePeriodRecords(records, opts) {
10194
10194
  ...basis ? { basis } : {}
10195
10195
  };
10196
10196
  accepted.push({
10197
- eventId: `${opts.tenantId}-usage-period-${++seq}`,
10197
+ eventId: periodFactId(opts.tenantId, ENERGY_EVENT.usagePeriod, meterId, span.from, span.to),
10198
10198
  eventType: ENERGY_EVENT.usagePeriod,
10199
10199
  /* 구간의 **끝**이 이 사실이 성립한 시각이다 — 시작으로 달면 저널이 그 구간을 미리 안 것이 된다. */
10200
10200
  eventTime: span.to,
@@ -10208,7 +10208,6 @@ function ingestEnergyBillRecords(records, opts) {
10208
10208
  const list = records === void 0 || records === null ? [] : Array.isArray(records) ? records : [records];
10209
10209
  const accepted = [];
10210
10210
  const rejected = [];
10211
- let seq = 0;
10212
10211
  for (const r of list) {
10213
10212
  const errors = [];
10214
10213
  const span = readSpan(r, errors);
@@ -10235,7 +10234,7 @@ function ingestEnergyBillRecords(records, opts) {
10235
10234
  ...billingDemandKW !== void 0 ? { billingDemandKW } : {}
10236
10235
  };
10237
10236
  accepted.push({
10238
- eventId: `${opts.tenantId}-bill-${++seq}`,
10237
+ eventId: periodFactId(opts.tenantId, ENERGY_EVENT.bill, "", span.from, span.to),
10239
10238
  eventType: ENERGY_EVENT.bill,
10240
10239
  eventTime: span.to,
10241
10240
  tenantId: opts.tenantId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/twin-kernel",
3
- "version": "0.7.80",
3
+ "version": "0.7.81",
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": {