@operato/twin-kernel 0.7.86 → 0.7.87

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.
@@ -35,6 +35,11 @@ export interface EnergyIngestOptions {
35
35
  * 그때도 `twin-local` 이되 범위는 비어 있다.
36
36
  */
37
37
  scopeId?: string;
38
+ /**
39
+ * **지금** — 아직 오지 않은 시각의 사실을 받지 않기 위해 쓴다. 주지 않으면 `defaultEventTime` 을 쓰고,
40
+ * 그것도 없으면 판단하지 않는다(없는 기준으로 거절하지 않는다).
41
+ */
42
+ nowISO?: string;
38
43
  }
39
44
  export interface EnergyIngestResult {
40
45
  accepted: CanonicalEnvelope[];
@@ -200,14 +205,6 @@ export interface EnergyBillRecord {
200
205
  billingDemandKW?: number;
201
206
  }
202
207
  export declare function isEnergyBillRecord(record: unknown): boolean;
203
- /**
204
- * 이 사실의 **주체를 무엇으로 부를까** — 선언이 있으면 그 이름, 없으면 이름표와 그 범위.
205
- *
206
- * 선언이 없을 때 트윈 범위를 이름에 넣는 이유는 **겹침을 숨기지 않기 위해서**다. 넣지 않으면 서로 다른
207
- * 설비의 같은 날이 한 사실이 되고, 여러 현장을 함께 계산하는 곳에서 한쪽이 조용히 사라진다(실측).
208
- * 넣되 `subjectBasis` 로 **이 이름이 트윈 안에서만 통한다**고 밝힌다 — 그릇으로 정한 이름을 고유한
209
- * 척하지 않는다.
210
- */
211
208
  export declare function resolveSubject(kind: 'equipment' | 'meter', localId: string, opts: EnergyIngestOptions): {
212
209
  subject: string;
213
210
  basis: 'declared' | 'twin-local';
@@ -452,6 +452,11 @@ export function ingestEnergyGenerationPeriodRecords(records, opts) {
452
452
  const subj = resolveSubject('equipment', equipmentId, opts);
453
453
  data.subject = subj.subject;
454
454
  data.subjectBasis = subj.basis;
455
+ const futureError = futureFactError(span.to, opts);
456
+ if (futureError) {
457
+ rejected.push({ record: r, errors: [futureError] });
458
+ continue;
459
+ }
455
460
  accepted.push({
456
461
  eventId: periodFactId(opts.tenantId, ENERGY_EVENT.generationPeriod, subj.subject, span.from, span.to),
457
462
  eventType: ENERGY_EVENT.generationPeriod,
@@ -498,6 +503,11 @@ export function ingestEnergyGenerationPriceRecords(records, opts) {
498
503
  continue;
499
504
  }
500
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
+ }
501
511
  accepted.push({
502
512
  eventId: periodFactId(opts.tenantId, ENERGY_EVENT.generationPrice, '', span.from, span.to),
503
513
  eventType: ENERGY_EVENT.generationPrice,
@@ -563,6 +573,11 @@ export function ingestEnergyTariffBasisRecords(records, opts) {
563
573
  ...(demandChargePerKW !== undefined ? { demandChargePerKW } : {}),
564
574
  ...(currency ? { currency } : {})
565
575
  };
576
+ const futureError = futureFactError(span.from, opts);
577
+ if (futureError) {
578
+ rejected.push({ record: r, errors: [futureError] });
579
+ continue;
580
+ }
566
581
  accepted.push({
567
582
  eventId: periodFactId(opts.tenantId, ENERGY_EVENT.tariffBasis, '', span.from, span.to),
568
583
  eventType: ENERGY_EVENT.tariffBasis,
@@ -607,6 +622,30 @@ export function isEnergyBillRecord(record) {
607
622
  * 넣되 `subjectBasis` 로 **이 이름이 트윈 안에서만 통한다**고 밝힌다 — 그릇으로 정한 이름을 고유한
608
623
  * 척하지 않는다.
609
624
  */
625
+ /**
626
+ * **아직 오지 않은 시각의 사실은 받지 않는다.**
627
+ *
628
+ * ── 왜 필요한가 (2026-08-30) ──────────────────────────────────────────────
629
+ * 한 원본이 **끝나지 않은 이번 달**의 요금 정보를 청구서로 보냈다. 청구서의 사건 시각은 기간의 끝이라
630
+ * 그 사실이 **내일** 일어난 것으로 저널에 적혔다. 성과 계산은 「가장 늦은 사건」으로 구간을 잡으므로
631
+ * 구간 전체가 미래로 밀렸고, 그 현장의 화면에서 어제 발전량이 사라졌다. 오류는 하나도 나지 않았다.
632
+ *
633
+ * 지난 기록 채우기는 이미 이 판단을 하고 있었다(§`judgeBackfill`). 라이브 문에만 없어서 **한 종류에
634
+ * 규칙이 두 벌**이었다. 같은 사실이 어느 문으로 들어오느냐에 따라 받아지기도 하고 거절되기도 했다.
635
+ *
636
+ * 기준 선언(요금 기준·발전 단가)은 기간이 미래로 뻗어도 된다 — 그쪽은 사건 시각이 **기간의 시작**이라
637
+ * 이 판정에 걸리지 않는다. 앞으로 이렇게 매기겠다는 선언은 오늘 성립한다.
638
+ */
639
+ function futureFactError(eventTime, opts) {
640
+ const nowText = opts.nowISO ?? opts.defaultEventTime;
641
+ if (!nowText)
642
+ return undefined;
643
+ const nowMs = Date.parse(nowText);
644
+ const atMs = Date.parse(eventTime);
645
+ if (!Number.isFinite(nowMs) || !Number.isFinite(atMs) || atMs <= nowMs)
646
+ return undefined;
647
+ return `아직 오지 않은 시각의 사실이다(${eventTime}) — 끝나지 않은 기간은 마감된 사실이 아니다`;
648
+ }
610
649
  export function resolveSubject(kind, localId, opts) {
611
650
  const declared = opts.identityOf?.(kind, localId);
612
651
  if (declared)
@@ -699,6 +738,11 @@ export function ingestEnergyUsagePeriodRecords(records, opts) {
699
738
  ...(currency ? { currency } : {}),
700
739
  ...(basis ? { basis } : {})
701
740
  };
741
+ const futureError = futureFactError(span.to, opts);
742
+ if (futureError) {
743
+ rejected.push({ record: r, errors: [futureError] });
744
+ continue;
745
+ }
702
746
  accepted.push({
703
747
  eventId: periodFactId(opts.tenantId, ENERGY_EVENT.usagePeriod, subj.subject, span.from, span.to),
704
748
  eventType: ENERGY_EVENT.usagePeriod,
@@ -745,6 +789,11 @@ export function ingestEnergyBillRecords(records, opts) {
745
789
  ...(currency ? { currency } : {}),
746
790
  ...(billingDemandKW !== undefined ? { billingDemandKW } : {})
747
791
  };
792
+ const futureError = futureFactError(span.to, opts);
793
+ if (futureError) {
794
+ rejected.push({ record: r, errors: [futureError] });
795
+ continue;
796
+ }
748
797
  accepted.push({
749
798
  eventId: periodFactId(opts.tenantId, ENERGY_EVENT.bill, '', span.from, span.to),
750
799
  eventType: ENERGY_EVENT.bill,
@@ -2824,6 +2824,11 @@ function ingestEnergyGenerationPeriodRecords(records, opts) {
2824
2824
  const subj = resolveSubject("equipment", equipmentId, opts);
2825
2825
  data.subject = subj.subject;
2826
2826
  data.subjectBasis = subj.basis;
2827
+ const futureError = futureFactError(span.to, opts);
2828
+ if (futureError) {
2829
+ rejected.push({ record: r, errors: [futureError] });
2830
+ continue;
2831
+ }
2827
2832
  accepted.push({
2828
2833
  eventId: periodFactId(opts.tenantId, ENERGY_EVENT.generationPeriod, subj.subject, span.from, span.to),
2829
2834
  eventType: ENERGY_EVENT.generationPeriod,
@@ -2857,6 +2862,11 @@ function ingestEnergyGenerationPriceRecords(records, opts) {
2857
2862
  continue;
2858
2863
  }
2859
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
+ }
2860
2870
  accepted.push({
2861
2871
  eventId: periodFactId(opts.tenantId, ENERGY_EVENT.generationPrice, "", span.from, span.to),
2862
2872
  eventType: ENERGY_EVENT.generationPrice,
@@ -2902,6 +2912,11 @@ function ingestEnergyTariffBasisRecords(records, opts) {
2902
2912
  ...demandChargePerKW !== void 0 ? { demandChargePerKW } : {},
2903
2913
  ...currency ? { currency } : {}
2904
2914
  };
2915
+ const futureError = futureFactError(span.from, opts);
2916
+ if (futureError) {
2917
+ rejected.push({ record: r, errors: [futureError] });
2918
+ continue;
2919
+ }
2905
2920
  accepted.push({
2906
2921
  eventId: periodFactId(opts.tenantId, ENERGY_EVENT.tariffBasis, "", span.from, span.to),
2907
2922
  eventType: ENERGY_EVENT.tariffBasis,
@@ -2919,6 +2934,14 @@ function isEnergyBillRecord(record) {
2919
2934
  if (r.from === void 0 || r.to === void 0) return false;
2920
2935
  return r.energyCharge !== void 0 || r.demandCharge !== void 0 || r.total !== void 0;
2921
2936
  }
2937
+ function futureFactError(eventTime, opts) {
2938
+ const nowText = opts.nowISO ?? opts.defaultEventTime;
2939
+ if (!nowText) return void 0;
2940
+ const nowMs = Date.parse(nowText);
2941
+ const atMs = Date.parse(eventTime);
2942
+ if (!Number.isFinite(nowMs) || !Number.isFinite(atMs) || atMs <= nowMs) return void 0;
2943
+ return `\uC544\uC9C1 \uC624\uC9C0 \uC54A\uC740 \uC2DC\uAC01\uC758 \uC0AC\uC2E4\uC774\uB2E4(${eventTime}) \u2014 \uB05D\uB098\uC9C0 \uC54A\uC740 \uAE30\uAC04\uC740 \uB9C8\uAC10\uB41C \uC0AC\uC2E4\uC774 \uC544\uB2C8\uB2E4`;
2944
+ }
2922
2945
  function resolveSubject(kind, localId, opts) {
2923
2946
  const declared = opts.identityOf?.(kind, localId);
2924
2947
  if (declared) return { subject: declared, basis: "declared" };
@@ -2997,6 +3020,11 @@ function ingestEnergyUsagePeriodRecords(records, opts) {
2997
3020
  ...currency ? { currency } : {},
2998
3021
  ...basis ? { basis } : {}
2999
3022
  };
3023
+ const futureError = futureFactError(span.to, opts);
3024
+ if (futureError) {
3025
+ rejected.push({ record: r, errors: [futureError] });
3026
+ continue;
3027
+ }
3000
3028
  accepted.push({
3001
3029
  eventId: periodFactId(opts.tenantId, ENERGY_EVENT.usagePeriod, subj.subject, span.from, span.to),
3002
3030
  eventType: ENERGY_EVENT.usagePeriod,
@@ -3037,6 +3065,11 @@ function ingestEnergyBillRecords(records, opts) {
3037
3065
  ...currency ? { currency } : {},
3038
3066
  ...billingDemandKW !== void 0 ? { billingDemandKW } : {}
3039
3067
  };
3068
+ const futureError = futureFactError(span.to, opts);
3069
+ if (futureError) {
3070
+ rejected.push({ record: r, errors: [futureError] });
3071
+ continue;
3072
+ }
3040
3073
  accepted.push({
3041
3074
  eventId: periodFactId(opts.tenantId, ENERGY_EVENT.bill, "", span.from, span.to),
3042
3075
  eventType: ENERGY_EVENT.bill,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/twin-kernel",
3
- "version": "0.7.86",
3
+ "version": "0.7.87",
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": {