@operato/twin-kernel 0.7.84 → 0.7.86
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/contract.d.ts +28 -0
- package/dist/ems-kernel.js +18 -5
- package/dist/energy-ingest.d.ts +28 -0
- package/dist/energy-ingest.js +23 -3
- package/dist/flow-engine.d.ts +37 -0
- package/dist/flow-engine.js +49 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist-cjs/index.cjs +7164 -7093
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -2743,6 +2743,27 @@ export interface EnergyMeasuredData {
|
|
|
2743
2743
|
* 않은 것이다. 태양광이면 대개 0 이지만 그것은 현장이 판단할 일이고, 재지 않은 것을 0 으로 적으면
|
|
2744
2744
|
* 우리가 그 판단을 대신한 것이 된다.
|
|
2745
2745
|
*/
|
|
2746
|
+
/**
|
|
2747
|
+
* 사실의 **주체를 무엇으로 불렀나** — 그 이름이 어디까지 통하는지가 여기서 갈린다.
|
|
2748
|
+
*
|
|
2749
|
+
* ── 왜 이 축이 필요한가 (2026-08-30) ──────────────────────────────────────
|
|
2750
|
+
* 마감된 구간 사실은 정체성이 내용에 있다(종류·주체·시작·끝). 그런데 **주체를 설비 번호로 불렀다.**
|
|
2751
|
+
* 설비 번호는 한 트윈 안에서만 통하는 이름표라, 한 도메인에 현장이 둘이면 서로 다른 설비의 같은 날이
|
|
2752
|
+
* **한 사실**이 된다. 실측으로 그 일이 났다 — 두 발전소의 `002` 가 겹쳐, 여러 현장을 함께 계산하는
|
|
2753
|
+
* 성과 화면에서 한쪽 발전량이 사라졌다.
|
|
2754
|
+
*
|
|
2755
|
+
* 고치는 방법은 두 가지였고 하나는 틀렸다. **트윈 id 를 이름에 붙이는 것은 그릇으로 정체성을 정하는
|
|
2756
|
+
* 것**이라 원칙을 뒤집는다(같은 설비를 현장 트윈과 상위 집계 트윈에 두면 같은 날이 두 사실이 된다).
|
|
2757
|
+
*
|
|
2758
|
+
* 그래서 이름의 출처를 밝힌다.
|
|
2759
|
+
*
|
|
2760
|
+
* declared 모델이 선언한 설비 정체성 — 그릇과 무관하게 고유하다
|
|
2761
|
+
* twin-local 선언이 없어 이름표를 그대로 썼다 — **이 트윈 안에서만 통한다**
|
|
2762
|
+
*
|
|
2763
|
+
* 커널은 없는 이름을 지어내지 않고, 겹치는 이름을 고유한 척하지도 않는다. 읽는 쪽은 `twin-local` 을
|
|
2764
|
+
* 볼 때 트윈 밖에서 같다고 판정하면 안 된다.
|
|
2765
|
+
*/
|
|
2766
|
+
export type SubjectBasis = 'declared' | 'twin-local';
|
|
2746
2767
|
export interface EnergyGenerationPeriodData {
|
|
2747
2768
|
equipmentId: string;
|
|
2748
2769
|
/** 이 기간에 만든 양(kWh). */
|
|
@@ -2756,6 +2777,9 @@ export interface EnergyGenerationPeriodData {
|
|
|
2756
2777
|
accumulation: 'lifetime' | 'daily' | 'monthly' | 'billing';
|
|
2757
2778
|
/** 기간의 경계가 어디서 왔나 — 원본의 기준점(`declared`)이거나 현장의 시각 기준(`offset`). */
|
|
2758
2779
|
boundary: 'declared' | 'offset';
|
|
2780
|
+
/** 이 사실이 무엇에 대한 것인가 — 정체성으로 부른 이름(§`SubjectBasis`). */
|
|
2781
|
+
subject?: string;
|
|
2782
|
+
subjectBasis?: SubjectBasis;
|
|
2759
2783
|
}
|
|
2760
2784
|
/**
|
|
2761
2785
|
* 값이 어디서 왔나 — **관측의 근거**.
|
|
@@ -2786,6 +2810,9 @@ export interface EnergyUsagePeriodData {
|
|
|
2786
2810
|
unitPrice?: number;
|
|
2787
2811
|
currency?: string;
|
|
2788
2812
|
basis?: ObservationBasis;
|
|
2813
|
+
/** 이 사실이 무엇에 대한 것인가 — 계량 지점도 설비와 같은 문제를 갖는다(§`SubjectBasis`). */
|
|
2814
|
+
subject?: string;
|
|
2815
|
+
subjectBasis?: SubjectBasis;
|
|
2789
2816
|
}
|
|
2790
2817
|
/**
|
|
2791
2818
|
* 청구서 — `ENERGY_EVENT.bill` 의 데이터.
|
|
@@ -3104,6 +3131,7 @@ export interface TwinModelDef {
|
|
|
3104
3131
|
id: string;
|
|
3105
3132
|
kind: string;
|
|
3106
3133
|
homeLocation: string;
|
|
3134
|
+
identity?: string;
|
|
3107
3135
|
mtbfMs?: number;
|
|
3108
3136
|
mttrMs?: number;
|
|
3109
3137
|
window?: {
|
package/dist/ems-kernel.js
CHANGED
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
* **관측 시작 이후 최대**만 답하고 그 사실을 이름으로 말한다(`peakSince`). 지어내지 않는다.
|
|
27
27
|
*/
|
|
28
28
|
import { FlowEngine } from "./flow-engine.js";
|
|
29
|
+
/* 기간 사실의 이름은 유입 문과 한 규칙이다(§`periodFactId`). */
|
|
30
|
+
import { periodFactId } from "./energy-ingest.js";
|
|
29
31
|
import { firstFitPolicy } from "./allocation-policy.js";
|
|
30
32
|
import { CMD, ENERGY_EVENT, minuteOfDayAt, localDayIndexAt, localDayStartMs } from "./contract.js";
|
|
31
33
|
import { EMS_PROPERTY, METER_DIRECTION, electricalUpstreamOf, generationFractionAt } from "./ems-profile.js";
|
|
@@ -659,16 +661,27 @@ export class EmsKernel extends FlowEngine {
|
|
|
659
661
|
*/
|
|
660
662
|
const total = obs.lastKWh;
|
|
661
663
|
if (Number.isFinite(total) && total >= 0) {
|
|
662
|
-
|
|
664
|
+
/*
|
|
665
|
+
* 이름을 **내용에서** 만든다 — 같은 하루를 커널이 닫은 것과 커넥터가 지난 기록으로 보낸 것이
|
|
666
|
+
* 한 사실이어야 한다(§`emitPeriodFact`).
|
|
667
|
+
*/
|
|
668
|
+
const periodStart = new Date(startMs).toISOString();
|
|
669
|
+
const periodEnd = new Date(endMs).toISOString();
|
|
670
|
+
const subj = this.subjectOf('equipment', id);
|
|
671
|
+
this.emitPeriodFact(ENERGY_EVENT.generationPeriod, {
|
|
663
672
|
equipmentId: id,
|
|
664
673
|
kWh: total,
|
|
665
|
-
periodStart
|
|
666
|
-
periodEnd
|
|
674
|
+
periodStart,
|
|
675
|
+
periodEnd,
|
|
667
676
|
observedFrom: new Date(obs.fromMs).toISOString(),
|
|
668
677
|
observedTo: new Date(obs.lastMs).toISOString(),
|
|
669
678
|
accumulation: at.kind,
|
|
670
|
-
boundary
|
|
671
|
-
|
|
679
|
+
boundary,
|
|
680
|
+
subject: subj.subject,
|
|
681
|
+
subjectBasis: subj.basis
|
|
682
|
+
}, periodFactId(this.tenantId, ENERGY_EVENT.generationPeriod, subj.subject, periodStart, periodEnd),
|
|
683
|
+
/* 이 사실이 성립한 것은 기간의 끝이다 — 우리가 계산한 시각이 아니다. */
|
|
684
|
+
periodEnd);
|
|
672
685
|
eq.generatedKWhLastPeriod = total;
|
|
673
686
|
eq.generatedKWhLastPeriodEnd = new Date(endMs).toISOString();
|
|
674
687
|
/* 상태만 읽는 화면이 덜 잰 값을 온전한 값으로 읽지 않게, 관측 구간을 함께 남긴다. */
|
package/dist/energy-ingest.d.ts
CHANGED
|
@@ -20,6 +20,21 @@ export interface EnergyIngestOptions {
|
|
|
20
20
|
tenantId: string;
|
|
21
21
|
/** 레코드에 시각이 없을 때 쓸 값 — **주지 않으면 그 레코드를 거부한다.** */
|
|
22
22
|
defaultEventTime?: string;
|
|
23
|
+
/**
|
|
24
|
+
* 자원의 **선언된 정체성**을 답한다 — 없으면 `undefined`.
|
|
25
|
+
*
|
|
26
|
+
* 마감된 구간 사실의 이름에 들어갈 주체를 이것으로 정한다(§`SubjectBasis`). 커널이 이름을
|
|
27
|
+
* 지어내지 않는 것과 같은 자세다: 모델이 선언하지 않았으면 답하지 않고, 호출부가 무엇을 할지 정한다.
|
|
28
|
+
*/
|
|
29
|
+
identityOf?: (kind: 'equipment' | 'meter', localId: string) => string | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* 선언된 정체성이 없을 때 이름표가 **어디까지 통하는지** — 보통 이 트윈의 id.
|
|
32
|
+
*
|
|
33
|
+
* 이름에 넣는 것이 아니라 **범위를 밝히는 데** 쓴다. 이것이 있으면 사실은 `twin-local` 로 표시되고,
|
|
34
|
+
* 읽는 쪽은 그 이름을 트윈 밖에서 같다고 판정하지 않는다. 주지 않으면 범위를 모르는 것이므로
|
|
35
|
+
* 그때도 `twin-local` 이되 범위는 비어 있다.
|
|
36
|
+
*/
|
|
37
|
+
scopeId?: string;
|
|
23
38
|
}
|
|
24
39
|
export interface EnergyIngestResult {
|
|
25
40
|
accepted: CanonicalEnvelope[];
|
|
@@ -185,5 +200,18 @@ export interface EnergyBillRecord {
|
|
|
185
200
|
billingDemandKW?: number;
|
|
186
201
|
}
|
|
187
202
|
export declare function isEnergyBillRecord(record: unknown): boolean;
|
|
203
|
+
/**
|
|
204
|
+
* 이 사실의 **주체를 무엇으로 부를까** — 선언이 있으면 그 이름, 없으면 이름표와 그 범위.
|
|
205
|
+
*
|
|
206
|
+
* 선언이 없을 때 트윈 범위를 이름에 넣는 이유는 **겹침을 숨기지 않기 위해서**다. 넣지 않으면 서로 다른
|
|
207
|
+
* 설비의 같은 날이 한 사실이 되고, 여러 현장을 함께 계산하는 곳에서 한쪽이 조용히 사라진다(실측).
|
|
208
|
+
* 넣되 `subjectBasis` 로 **이 이름이 트윈 안에서만 통한다**고 밝힌다 — 그릇으로 정한 이름을 고유한
|
|
209
|
+
* 척하지 않는다.
|
|
210
|
+
*/
|
|
211
|
+
export declare function resolveSubject(kind: 'equipment' | 'meter', localId: string, opts: EnergyIngestOptions): {
|
|
212
|
+
subject: string;
|
|
213
|
+
basis: 'declared' | 'twin-local';
|
|
214
|
+
};
|
|
215
|
+
export declare function periodFactId(tenantId: string, kind: string, subject: string, from: string, to: string): string;
|
|
188
216
|
export declare function ingestEnergyUsagePeriodRecords(records: EnergyUsagePeriodRecord | EnergyUsagePeriodRecord[] | undefined | null, opts: EnergyIngestOptions): EnergyIngestResult;
|
|
189
217
|
export declare function ingestEnergyBillRecords(records: EnergyBillRecord | EnergyBillRecord[] | undefined | null, opts: EnergyIngestOptions): EnergyIngestResult;
|
package/dist/energy-ingest.js
CHANGED
|
@@ -449,8 +449,11 @@ export function ingestEnergyGenerationPeriodRecords(records, opts) {
|
|
|
449
449
|
/* 밖에서 마감되어 온 것이다 — 우리가 시각 기준으로 닫은 것이 아니다. */
|
|
450
450
|
boundary: 'declared'
|
|
451
451
|
};
|
|
452
|
+
const subj = resolveSubject('equipment', equipmentId, opts);
|
|
453
|
+
data.subject = subj.subject;
|
|
454
|
+
data.subjectBasis = subj.basis;
|
|
452
455
|
accepted.push({
|
|
453
|
-
eventId: periodFactId(opts.tenantId, ENERGY_EVENT.generationPeriod,
|
|
456
|
+
eventId: periodFactId(opts.tenantId, ENERGY_EVENT.generationPeriod, subj.subject, span.from, span.to),
|
|
454
457
|
eventType: ENERGY_EVENT.generationPeriod,
|
|
455
458
|
eventTime: span.to,
|
|
456
459
|
tenantId: opts.tenantId,
|
|
@@ -596,7 +599,21 @@ export function isEnergyBillRecord(record) {
|
|
|
596
599
|
* **이것만으로 중복이 막히지는 않는다.** 저널은 아직 이 id 를 유일성으로 쓰지 않는다(그 판단은
|
|
597
600
|
* 호스트의 것이고 드라이버마다 다르다). 여기서 하는 일은 **막을 근거를 만드는 것**이다.
|
|
598
601
|
*/
|
|
599
|
-
|
|
602
|
+
/**
|
|
603
|
+
* 이 사실의 **주체를 무엇으로 부를까** — 선언이 있으면 그 이름, 없으면 이름표와 그 범위.
|
|
604
|
+
*
|
|
605
|
+
* 선언이 없을 때 트윈 범위를 이름에 넣는 이유는 **겹침을 숨기지 않기 위해서**다. 넣지 않으면 서로 다른
|
|
606
|
+
* 설비의 같은 날이 한 사실이 되고, 여러 현장을 함께 계산하는 곳에서 한쪽이 조용히 사라진다(실측).
|
|
607
|
+
* 넣되 `subjectBasis` 로 **이 이름이 트윈 안에서만 통한다**고 밝힌다 — 그릇으로 정한 이름을 고유한
|
|
608
|
+
* 척하지 않는다.
|
|
609
|
+
*/
|
|
610
|
+
export function resolveSubject(kind, localId, opts) {
|
|
611
|
+
const declared = opts.identityOf?.(kind, localId);
|
|
612
|
+
if (declared)
|
|
613
|
+
return { subject: declared, basis: 'declared' };
|
|
614
|
+
return { subject: opts.scopeId ? `${opts.scopeId}/${localId}` : localId, basis: 'twin-local' };
|
|
615
|
+
}
|
|
616
|
+
export function periodFactId(tenantId, kind, subject, from, to) {
|
|
600
617
|
/* 대상이 없는 사실(요금 기준·청구서·발전 단가)은 그 자리를 비운다 — 없는 것을 지어내지 않는다. */
|
|
601
618
|
return [tenantId, kind, subject, from, to].join('|');
|
|
602
619
|
}
|
|
@@ -669,8 +686,11 @@ export function ingestEnergyUsagePeriodRecords(records, opts) {
|
|
|
669
686
|
rejected.push({ record: r, errors });
|
|
670
687
|
continue;
|
|
671
688
|
}
|
|
689
|
+
const subj = resolveSubject('meter', meterId, opts);
|
|
672
690
|
const data = {
|
|
673
691
|
meterId,
|
|
692
|
+
subject: subj.subject,
|
|
693
|
+
subjectBasis: subj.basis,
|
|
674
694
|
from: span.from,
|
|
675
695
|
to: span.to,
|
|
676
696
|
kWh,
|
|
@@ -680,7 +700,7 @@ export function ingestEnergyUsagePeriodRecords(records, opts) {
|
|
|
680
700
|
...(basis ? { basis } : {})
|
|
681
701
|
};
|
|
682
702
|
accepted.push({
|
|
683
|
-
eventId: periodFactId(opts.tenantId, ENERGY_EVENT.usagePeriod,
|
|
703
|
+
eventId: periodFactId(opts.tenantId, ENERGY_EVENT.usagePeriod, subj.subject, span.from, span.to),
|
|
684
704
|
eventType: ENERGY_EVENT.usagePeriod,
|
|
685
705
|
/* 구간의 **끝**이 이 사실이 성립한 시각이다 — 시작으로 달면 저널이 그 구간을 미리 안 것이 된다. */
|
|
686
706
|
eventTime: span.to,
|
package/dist/flow-engine.d.ts
CHANGED
|
@@ -1282,6 +1282,43 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
1282
1282
|
*/
|
|
1283
1283
|
private _correlationId?;
|
|
1284
1284
|
protected emit(event: EpcisEvent): void;
|
|
1285
|
+
/**
|
|
1286
|
+
* **마감된 구간 사실**을 낸다 — 이름을 내용에서 만든다(순번이 아니다).
|
|
1287
|
+
*
|
|
1288
|
+
* ── 왜 따로 있나 (2026-08-30) ────────────────────────────────────────────
|
|
1289
|
+
* `emitOp` 은 이름을 순번으로 붙인다(`…-evt-3`). 관측이나 전이는 그래도 된다 — 두 번 일어나면 두
|
|
1290
|
+
* 사실이다. 그런데 **마감된 구간은 같은 기간이면 한 사실이다.** 같은 하루를 커널이 자정에 닫고
|
|
1291
|
+
* 커넥터가 지난 기록으로도 보내면, 순번 이름으로는 그 둘이 다른 사실이 되어 **발전량이 두 배로**
|
|
1292
|
+
* 잡힌다. 유입 문은 이미 내용으로 이름을 짓고 있었는데(§`periodFactId`) 커널 자신만 아니었다 —
|
|
1293
|
+
* 한 종류에 규칙이 두 벌이었다.
|
|
1294
|
+
*
|
|
1295
|
+
* ── 시각도 내용에서 온다 ─────────────────────────────────────────────────
|
|
1296
|
+
* `emitOp` 은 **지금**을 찍는다. 마감된 구간에 그것을 찍으면 「우리가 언제 계산했나」가 되고, 재기동이
|
|
1297
|
+
* 늦으면 어제의 사실이 오늘 시각을 갖는다. 실제로 그렇게 났다 — 부팅에서 닫힌 8월 29일치가 22:24 를
|
|
1298
|
+
* 달고 나왔고, 시각 구간으로 읽는 중복 판정이 그것을 못 봐서 같은 하루가 12줄이 되었다(이름은 6개).
|
|
1299
|
+
*
|
|
1300
|
+
* 이 사실이 성립하는 것은 **기간의 끝**이다. 유입 문도 그 시각을 찍는다.
|
|
1301
|
+
*/
|
|
1302
|
+
protected emitPeriodFact(eventType: string, data: unknown, eventId: string, eventTime: string): void;
|
|
1303
|
+
/**
|
|
1304
|
+
* 이 트윈의 **범위 이름** — 선언된 정체성이 없을 때 이름표가 어디까지 통하는지(§`SubjectBasis`).
|
|
1305
|
+
*
|
|
1306
|
+
* 호스트가 세운다. 커널은 `new Kernel(domainId, …)` 으로 서므로 **자기가 어느 트윈인지 모른다** —
|
|
1307
|
+
* 도메인에 현장이 둘이면 설비 번호가 겹치는데 커널만으로는 그것을 알 길이 없었다.
|
|
1308
|
+
*/
|
|
1309
|
+
scopeId?: string;
|
|
1310
|
+
/**
|
|
1311
|
+
* 설비의 **선언된 정체성** — 모델이 적었으면 그 이름, 없으면 답하지 않는다.
|
|
1312
|
+
*
|
|
1313
|
+
* 물품과 같은 자세다(§`declaredObjectId`): 선언이 없으면 지어내지 않는다. 명시한 이름이 먼저이고,
|
|
1314
|
+
* 없으면 선언된 이름공간 아래의 개체 식별자로 만든다(CBV §8.2.3·§8.2.4).
|
|
1315
|
+
*/
|
|
1316
|
+
equipmentIdentity(equipmentId: string): string | undefined;
|
|
1317
|
+
/** 유입 문과 **같은 규칙**으로 주체를 정한다 — 이름 짓는 자리가 둘이면 한쪽만 고쳐지는 날이 온다. */
|
|
1318
|
+
protected subjectOf(kind: 'equipment' | 'meter', localId: string): {
|
|
1319
|
+
subject: string;
|
|
1320
|
+
basis: 'declared' | 'twin-local';
|
|
1321
|
+
};
|
|
1285
1322
|
protected emitOp(eventType: string, data: unknown): void;
|
|
1286
1323
|
/**
|
|
1287
1324
|
* 작업 전이 방출 — **커널이 아는 것을 미러도 알게** 한다.
|
package/dist/flow-engine.js
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { OP_EVENT, CMD, locationStatusOf, readBoardEquipment, readBoardLocations, readBoardAssets, classClosure, capabilityOf, requiredTestsFor, priorityRank, dueStatusOf, isOrderTerminal, effectivityAt, offCalendarAt, offCalendarReasonAt, minuteOfDayAt, activeShiftAt, subLotIdOf, itemKeyOf, identityGroundingOf, outsideLimit } from "./contract.js";
|
|
13
13
|
import { ObservedReducer } from "./observed-reducer.js";
|
|
14
|
+
/* 주체를 정하는 규칙은 유입 문과 한 벌이다 — 두 곳에 적으면 한쪽만 고쳐진다. */
|
|
15
|
+
import { resolveSubject } from "./energy-ingest.js";
|
|
14
16
|
import { transformationEvent, aggregationEvent, objectEvent, parseEpc, DISP, ILMD_ATTR, CBV_BIZSTEP, objectUri, bizTransactionUri, gdtiUri } from "./epcis.js";
|
|
15
17
|
import { OP_PARAM } from "./domain-definition.js";
|
|
16
18
|
import { parseIsoDuration } from "./iso-duration.js";
|
|
@@ -2787,6 +2789,53 @@ export class FlowEngine {
|
|
|
2787
2789
|
for (const h of this.handlers)
|
|
2788
2790
|
h(e);
|
|
2789
2791
|
}
|
|
2792
|
+
/**
|
|
2793
|
+
* **마감된 구간 사실**을 낸다 — 이름을 내용에서 만든다(순번이 아니다).
|
|
2794
|
+
*
|
|
2795
|
+
* ── 왜 따로 있나 (2026-08-30) ────────────────────────────────────────────
|
|
2796
|
+
* `emitOp` 은 이름을 순번으로 붙인다(`…-evt-3`). 관측이나 전이는 그래도 된다 — 두 번 일어나면 두
|
|
2797
|
+
* 사실이다. 그런데 **마감된 구간은 같은 기간이면 한 사실이다.** 같은 하루를 커널이 자정에 닫고
|
|
2798
|
+
* 커넥터가 지난 기록으로도 보내면, 순번 이름으로는 그 둘이 다른 사실이 되어 **발전량이 두 배로**
|
|
2799
|
+
* 잡힌다. 유입 문은 이미 내용으로 이름을 짓고 있었는데(§`periodFactId`) 커널 자신만 아니었다 —
|
|
2800
|
+
* 한 종류에 규칙이 두 벌이었다.
|
|
2801
|
+
*
|
|
2802
|
+
* ── 시각도 내용에서 온다 ─────────────────────────────────────────────────
|
|
2803
|
+
* `emitOp` 은 **지금**을 찍는다. 마감된 구간에 그것을 찍으면 「우리가 언제 계산했나」가 되고, 재기동이
|
|
2804
|
+
* 늦으면 어제의 사실이 오늘 시각을 갖는다. 실제로 그렇게 났다 — 부팅에서 닫힌 8월 29일치가 22:24 를
|
|
2805
|
+
* 달고 나왔고, 시각 구간으로 읽는 중복 판정이 그것을 못 봐서 같은 하루가 12줄이 되었다(이름은 6개).
|
|
2806
|
+
*
|
|
2807
|
+
* 이 사실이 성립하는 것은 **기간의 끝**이다. 유입 문도 그 시각을 찍는다.
|
|
2808
|
+
*/
|
|
2809
|
+
emitPeriodFact(eventType, data, eventId, eventTime) {
|
|
2810
|
+
this.revision++;
|
|
2811
|
+
const e = { eventId, eventType, eventTime, tenantId: this.tenantId, ...(this._correlationId ? { correlationId: this._correlationId } : {}), data };
|
|
2812
|
+
for (const h of this.handlers)
|
|
2813
|
+
h(e);
|
|
2814
|
+
}
|
|
2815
|
+
/**
|
|
2816
|
+
* 이 트윈의 **범위 이름** — 선언된 정체성이 없을 때 이름표가 어디까지 통하는지(§`SubjectBasis`).
|
|
2817
|
+
*
|
|
2818
|
+
* 호스트가 세운다. 커널은 `new Kernel(domainId, …)` 으로 서므로 **자기가 어느 트윈인지 모른다** —
|
|
2819
|
+
* 도메인에 현장이 둘이면 설비 번호가 겹치는데 커널만으로는 그것을 알 길이 없었다.
|
|
2820
|
+
*/
|
|
2821
|
+
scopeId;
|
|
2822
|
+
/**
|
|
2823
|
+
* 설비의 **선언된 정체성** — 모델이 적었으면 그 이름, 없으면 답하지 않는다.
|
|
2824
|
+
*
|
|
2825
|
+
* 물품과 같은 자세다(§`declaredObjectId`): 선언이 없으면 지어내지 않는다. 명시한 이름이 먼저이고,
|
|
2826
|
+
* 없으면 선언된 이름공간 아래의 개체 식별자로 만든다(CBV §8.2.3·§8.2.4).
|
|
2827
|
+
*/
|
|
2828
|
+
equipmentIdentity(equipmentId) {
|
|
2829
|
+
const decl = (this.boardDef?.equipment ?? []).find(e => e.id === equipmentId);
|
|
2830
|
+
const explicit = String(decl?.identity ?? '').trim();
|
|
2831
|
+
if (explicit)
|
|
2832
|
+
return explicit;
|
|
2833
|
+
return this.declaredObjectId(equipmentId);
|
|
2834
|
+
}
|
|
2835
|
+
/** 유입 문과 **같은 규칙**으로 주체를 정한다 — 이름 짓는 자리가 둘이면 한쪽만 고쳐지는 날이 온다. */
|
|
2836
|
+
subjectOf(kind, localId) {
|
|
2837
|
+
return resolveSubject(kind, localId, { tenantId: this.tenantId, scopeId: this.scopeId, identityOf: (k, id) => (k === 'equipment' ? this.equipmentIdentity(id) : undefined) });
|
|
2838
|
+
}
|
|
2790
2839
|
emitOp(eventType, data) {
|
|
2791
2840
|
this.revision++;
|
|
2792
2841
|
const e = { eventId: `${this.tenantId}-evt-${++this.eventSeq}`, eventType, eventTime: this.now(), tenantId: this.tenantId, ...(this._correlationId ? { correlationId: this._correlationId } : {}), data };
|
package/dist/index.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export { WmsKernel } from './kernel.ts';
|
|
|
30
30
|
export { YmsKernel } from './yms-kernel.ts';
|
|
31
31
|
export { MesKernel } from './mes-kernel.ts';
|
|
32
32
|
export { EmsKernel, DEMAND_WINDOW_MS, demandWindowStart } from './ems-kernel.ts';
|
|
33
|
-
export { ingestEnergyRecords, isEnergyRecord, ingestEnergyEquipmentRecords, isEnergyEquipmentRecord, ingestEnergyGenerationRecords, isEnergyGenerationRecord, ingestEnergyUsagePeriodRecords, isEnergyUsagePeriodRecord, ingestEnergyBillRecords, isEnergyBillRecord, ingestEnergyTariffBasisRecords, isEnergyTariffBasisRecord, ingestEnergyGenerationPriceRecords, isEnergyGenerationPriceRecord, ingestEnergyGenerationPeriodRecords, isEnergyGenerationPeriodRecord } from './energy-ingest.ts';
|
|
33
|
+
export { ingestEnergyRecords, isEnergyRecord, ingestEnergyEquipmentRecords, isEnergyEquipmentRecord, ingestEnergyGenerationRecords, isEnergyGenerationRecord, ingestEnergyUsagePeriodRecords, isEnergyUsagePeriodRecord, ingestEnergyBillRecords, isEnergyBillRecord, ingestEnergyTariffBasisRecords, isEnergyTariffBasisRecord, ingestEnergyGenerationPriceRecords, isEnergyGenerationPriceRecord, ingestEnergyGenerationPeriodRecords, isEnergyGenerationPeriodRecord, resolveSubject, periodFactId } from './energy-ingest.ts';
|
|
34
34
|
export { ingestOperationalRecords, isOperationalRecord, operationalKindOf } from './operational-ingest.ts';
|
|
35
35
|
export type { OperationalKind, OperationalRecord, OperationalIngestOptions } from './operational-ingest.ts';
|
|
36
36
|
export { attributeEnergy, electricityCost, energyIntensity, energyOfWindows } from './energy-attribution.ts';
|
package/dist/index.js
CHANGED
|
@@ -30,7 +30,7 @@ export { WmsKernel } from "./kernel.js";
|
|
|
30
30
|
export { YmsKernel } from "./yms-kernel.js";
|
|
31
31
|
export { MesKernel } from "./mes-kernel.js";
|
|
32
32
|
export { EmsKernel, DEMAND_WINDOW_MS, demandWindowStart } from "./ems-kernel.js";
|
|
33
|
-
export { ingestEnergyRecords, isEnergyRecord, ingestEnergyEquipmentRecords, isEnergyEquipmentRecord, ingestEnergyGenerationRecords, isEnergyGenerationRecord, ingestEnergyUsagePeriodRecords, isEnergyUsagePeriodRecord, ingestEnergyBillRecords, isEnergyBillRecord, ingestEnergyTariffBasisRecords, isEnergyTariffBasisRecord, ingestEnergyGenerationPriceRecords, isEnergyGenerationPriceRecord, ingestEnergyGenerationPeriodRecords, isEnergyGenerationPeriodRecord } from "./energy-ingest.js";
|
|
33
|
+
export { ingestEnergyRecords, isEnergyRecord, ingestEnergyEquipmentRecords, isEnergyEquipmentRecord, ingestEnergyGenerationRecords, isEnergyGenerationRecord, ingestEnergyUsagePeriodRecords, isEnergyUsagePeriodRecord, ingestEnergyBillRecords, isEnergyBillRecord, ingestEnergyTariffBasisRecords, isEnergyTariffBasisRecord, ingestEnergyGenerationPriceRecords, isEnergyGenerationPriceRecord, ingestEnergyGenerationPeriodRecords, isEnergyGenerationPeriodRecord, resolveSubject, periodFactId } from "./energy-ingest.js";
|
|
34
34
|
/* 운영 사실의 문 — 리듀서가 다루는 여섯이 들어오는 자리(미러가 시뮬보다 가난하지 않게). */
|
|
35
35
|
export { ingestOperationalRecords, isOperationalRecord, operationalKindOf } from "./operational-ingest.js";
|
|
36
36
|
export { attributeEnergy, electricityCost, energyIntensity, energyOfWindows } from "./energy-attribution.js";
|