@operato/twin-kernel 0.7.85 → 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.
- package/dist/ems-kernel.js +3 -1
- package/dist/energy-ingest.d.ts +5 -8
- package/dist/energy-ingest.js +49 -0
- package/dist/flow-engine.d.ts +8 -1
- package/dist/flow-engine.js +9 -2
- package/dist-cjs/index.cjs +45 -3
- package/package.json +1 -1
package/dist/ems-kernel.js
CHANGED
|
@@ -679,7 +679,9 @@ export class EmsKernel extends FlowEngine {
|
|
|
679
679
|
boundary,
|
|
680
680
|
subject: subj.subject,
|
|
681
681
|
subjectBasis: subj.basis
|
|
682
|
-
}, periodFactId(this.tenantId, ENERGY_EVENT.generationPeriod, subj.subject, periodStart, periodEnd)
|
|
682
|
+
}, periodFactId(this.tenantId, ENERGY_EVENT.generationPeriod, subj.subject, periodStart, periodEnd),
|
|
683
|
+
/* 이 사실이 성립한 것은 기간의 끝이다 — 우리가 계산한 시각이 아니다. */
|
|
684
|
+
periodEnd);
|
|
683
685
|
eq.generatedKWhLastPeriod = total;
|
|
684
686
|
eq.generatedKWhLastPeriodEnd = new Date(endMs).toISOString();
|
|
685
687
|
/* 상태만 읽는 화면이 덜 잰 값을 온전한 값으로 읽지 않게, 관측 구간을 함께 남긴다. */
|
package/dist/energy-ingest.d.ts
CHANGED
|
@@ -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';
|
package/dist/energy-ingest.js
CHANGED
|
@@ -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,
|
package/dist/flow-engine.d.ts
CHANGED
|
@@ -1291,8 +1291,15 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
1291
1291
|
* 커넥터가 지난 기록으로도 보내면, 순번 이름으로는 그 둘이 다른 사실이 되어 **발전량이 두 배로**
|
|
1292
1292
|
* 잡힌다. 유입 문은 이미 내용으로 이름을 짓고 있었는데(§`periodFactId`) 커널 자신만 아니었다 —
|
|
1293
1293
|
* 한 종류에 규칙이 두 벌이었다.
|
|
1294
|
+
*
|
|
1295
|
+
* ── 시각도 내용에서 온다 ─────────────────────────────────────────────────
|
|
1296
|
+
* `emitOp` 은 **지금**을 찍는다. 마감된 구간에 그것을 찍으면 「우리가 언제 계산했나」가 되고, 재기동이
|
|
1297
|
+
* 늦으면 어제의 사실이 오늘 시각을 갖는다. 실제로 그렇게 났다 — 부팅에서 닫힌 8월 29일치가 22:24 를
|
|
1298
|
+
* 달고 나왔고, 시각 구간으로 읽는 중복 판정이 그것을 못 봐서 같은 하루가 12줄이 되었다(이름은 6개).
|
|
1299
|
+
*
|
|
1300
|
+
* 이 사실이 성립하는 것은 **기간의 끝**이다. 유입 문도 그 시각을 찍는다.
|
|
1294
1301
|
*/
|
|
1295
|
-
protected emitPeriodFact(eventType: string, data: unknown, eventId: string): void;
|
|
1302
|
+
protected emitPeriodFact(eventType: string, data: unknown, eventId: string, eventTime: string): void;
|
|
1296
1303
|
/**
|
|
1297
1304
|
* 이 트윈의 **범위 이름** — 선언된 정체성이 없을 때 이름표가 어디까지 통하는지(§`SubjectBasis`).
|
|
1298
1305
|
*
|
package/dist/flow-engine.js
CHANGED
|
@@ -2798,10 +2798,17 @@ export class FlowEngine {
|
|
|
2798
2798
|
* 커넥터가 지난 기록으로도 보내면, 순번 이름으로는 그 둘이 다른 사실이 되어 **발전량이 두 배로**
|
|
2799
2799
|
* 잡힌다. 유입 문은 이미 내용으로 이름을 짓고 있었는데(§`periodFactId`) 커널 자신만 아니었다 —
|
|
2800
2800
|
* 한 종류에 규칙이 두 벌이었다.
|
|
2801
|
+
*
|
|
2802
|
+
* ── 시각도 내용에서 온다 ─────────────────────────────────────────────────
|
|
2803
|
+
* `emitOp` 은 **지금**을 찍는다. 마감된 구간에 그것을 찍으면 「우리가 언제 계산했나」가 되고, 재기동이
|
|
2804
|
+
* 늦으면 어제의 사실이 오늘 시각을 갖는다. 실제로 그렇게 났다 — 부팅에서 닫힌 8월 29일치가 22:24 를
|
|
2805
|
+
* 달고 나왔고, 시각 구간으로 읽는 중복 판정이 그것을 못 봐서 같은 하루가 12줄이 되었다(이름은 6개).
|
|
2806
|
+
*
|
|
2807
|
+
* 이 사실이 성립하는 것은 **기간의 끝**이다. 유입 문도 그 시각을 찍는다.
|
|
2801
2808
|
*/
|
|
2802
|
-
emitPeriodFact(eventType, data, eventId) {
|
|
2809
|
+
emitPeriodFact(eventType, data, eventId, eventTime) {
|
|
2803
2810
|
this.revision++;
|
|
2804
|
-
const e = { eventId, eventType, eventTime
|
|
2811
|
+
const e = { eventId, eventType, eventTime, tenantId: this.tenantId, ...(this._correlationId ? { correlationId: this._correlationId } : {}), data };
|
|
2805
2812
|
for (const h of this.handlers)
|
|
2806
2813
|
h(e);
|
|
2807
2814
|
}
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -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,
|
|
@@ -5645,10 +5678,17 @@ var FlowEngine = class {
|
|
|
5645
5678
|
* 커넥터가 지난 기록으로도 보내면, 순번 이름으로는 그 둘이 다른 사실이 되어 **발전량이 두 배로**
|
|
5646
5679
|
* 잡힌다. 유입 문은 이미 내용으로 이름을 짓고 있었는데(§`periodFactId`) 커널 자신만 아니었다 —
|
|
5647
5680
|
* 한 종류에 규칙이 두 벌이었다.
|
|
5681
|
+
*
|
|
5682
|
+
* ── 시각도 내용에서 온다 ─────────────────────────────────────────────────
|
|
5683
|
+
* `emitOp` 은 **지금**을 찍는다. 마감된 구간에 그것을 찍으면 「우리가 언제 계산했나」가 되고, 재기동이
|
|
5684
|
+
* 늦으면 어제의 사실이 오늘 시각을 갖는다. 실제로 그렇게 났다 — 부팅에서 닫힌 8월 29일치가 22:24 를
|
|
5685
|
+
* 달고 나왔고, 시각 구간으로 읽는 중복 판정이 그것을 못 봐서 같은 하루가 12줄이 되었다(이름은 6개).
|
|
5686
|
+
*
|
|
5687
|
+
* 이 사실이 성립하는 것은 **기간의 끝**이다. 유입 문도 그 시각을 찍는다.
|
|
5648
5688
|
*/
|
|
5649
|
-
emitPeriodFact(eventType, data, eventId) {
|
|
5689
|
+
emitPeriodFact(eventType, data, eventId, eventTime) {
|
|
5650
5690
|
this.revision++;
|
|
5651
|
-
const e = { eventId, eventType, eventTime
|
|
5691
|
+
const e = { eventId, eventType, eventTime, tenantId: this.tenantId, ...this._correlationId ? { correlationId: this._correlationId } : {}, data };
|
|
5652
5692
|
for (const h of this.handlers) h(e);
|
|
5653
5693
|
}
|
|
5654
5694
|
/**
|
|
@@ -8840,7 +8880,9 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8840
8880
|
subject: subj.subject,
|
|
8841
8881
|
subjectBasis: subj.basis
|
|
8842
8882
|
},
|
|
8843
|
-
periodFactId(this.tenantId, ENERGY_EVENT.generationPeriod, subj.subject, periodStart, periodEnd)
|
|
8883
|
+
periodFactId(this.tenantId, ENERGY_EVENT.generationPeriod, subj.subject, periodStart, periodEnd),
|
|
8884
|
+
/* 이 사실이 성립한 것은 기간의 끝이다 — 우리가 계산한 시각이 아니다. */
|
|
8885
|
+
periodEnd
|
|
8844
8886
|
);
|
|
8845
8887
|
eq.generatedKWhLastPeriod = total;
|
|
8846
8888
|
eq.generatedKWhLastPeriodEnd = new Date(endMs).toISOString();
|
package/package.json
CHANGED