@operato/twin-kernel 0.7.83 → 0.7.84

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.
@@ -115,6 +115,34 @@ export interface EnergyUsagePeriodRecord {
115
115
  basis?: string;
116
116
  }
117
117
  export declare function isEnergyUsagePeriodRecord(record: unknown): boolean;
118
+ /**
119
+ * 마감된 **발전 기간** — 「이 기간에 이 설비가 이만큼 냈다」.
120
+ *
121
+ * 라이브에서는 커널이 스스로 마감해 낸다(§`closeGenerationPeriod`). 이 문은 **지난 기록을 채울 때**
122
+ * 쓴다 — 원본이 날짜별 발전량을 주는 현장에서, 트윈이 없던 동안의 날들을 뒤늦게 넣는다.
123
+ *
124
+ * 상태를 바꾸지 않는다. 저널에만 적히고, 저널을 읽는 쪽(성과 계산)이 그 사실을 본다.
125
+ */
126
+ export interface EnergyGenerationPeriodRecord {
127
+ equipmentId: string;
128
+ from: string;
129
+ to: string;
130
+ kWh: number;
131
+ /** 원본이 준 적산의 종류 — 총량을 어떻게 얻었는지가 이것으로 갈린다. */
132
+ accumulation?: string;
133
+ /** 그 기간에서 처음·마지막으로 관측한 시각. 기간 경계와 다르면 그만큼 재지 않았다. */
134
+ observedFrom?: string;
135
+ observedTo?: string;
136
+ }
137
+ export declare function isEnergyGenerationPeriodRecord(record: unknown): boolean;
138
+ /**
139
+ * 마감된 발전 기간을 봉투로 — **사건 시각은 기간의 끝**이다(그때 성립한다).
140
+ *
141
+ * 적산의 문(`ingestEnergyGenerationRecords`)과 겹치지 않는다: 그쪽은 구간이 없는 시점의 값이고
142
+ * 이쪽은 구간이 있는 마감된 사실이다. 겹치면 지난 기록이 「지금 적산」으로 읽혀 트윈의 시계가
143
+ * 과거로 끌린다.
144
+ */
145
+ export declare function ingestEnergyGenerationPeriodRecords(records: EnergyGenerationPeriodRecord | EnergyGenerationPeriodRecord[] | undefined | null, opts: EnergyIngestOptions): EnergyIngestResult;
118
146
  /** 발전 단가 — 이 기간에 낸 전기 1kWh 의 값. 날마다 바뀐다. */
119
147
  export interface EnergyGenerationPriceRecord {
120
148
  from: string;
@@ -268,6 +268,9 @@ export function ingestEnergyEquipmentRecords(records, opts) {
268
268
  }
269
269
  /** 이 레코드가 발전 적산인가 — 어느 갈래로 보낼지를 한 곳에서 정한다. */
270
270
  export function isEnergyGenerationRecord(record) {
271
+ /* 구간이 있으면 마감된 발전 기간이지 시점의 적산이 아니다 — 겹치면 지난 기록이 「지금」으로 읽힌다. */
272
+ if (record?.from !== undefined || record?.to !== undefined)
273
+ return false;
271
274
  if (!record || typeof record !== 'object')
272
275
  return false;
273
276
  const r = record;
@@ -378,6 +381,84 @@ export function isEnergyUsagePeriodRecord(record) {
378
381
  const r = record;
379
382
  return !!r && typeof r === 'object' && r.meterId !== undefined && r.from !== undefined && r.to !== undefined && r.kWh !== undefined;
380
383
  }
384
+ export function isEnergyGenerationPeriodRecord(record) {
385
+ const r = record;
386
+ if (!r || typeof r !== 'object')
387
+ return false;
388
+ if (r.equipmentId === undefined || r.from === undefined || r.to === undefined)
389
+ return false;
390
+ return r.kWh !== undefined;
391
+ }
392
+ /**
393
+ * 마감된 발전 기간을 봉투로 — **사건 시각은 기간의 끝**이다(그때 성립한다).
394
+ *
395
+ * 적산의 문(`ingestEnergyGenerationRecords`)과 겹치지 않는다: 그쪽은 구간이 없는 시점의 값이고
396
+ * 이쪽은 구간이 있는 마감된 사실이다. 겹치면 지난 기록이 「지금 적산」으로 읽혀 트윈의 시계가
397
+ * 과거로 끌린다.
398
+ */
399
+ export function ingestEnergyGenerationPeriodRecords(records, opts) {
400
+ const list = records === undefined || records === null ? [] : Array.isArray(records) ? records : [records];
401
+ const accepted = [];
402
+ const rejected = [];
403
+ for (const r of list) {
404
+ const errors = [];
405
+ const equipmentId = String(r?.equipmentId ?? '').trim();
406
+ if (!equipmentId)
407
+ errors.push('equipmentId 가 없다 — 어느 설비가 낸 것인지 지어낼 수 없다');
408
+ const span = readSpan(r, errors);
409
+ const kWh = readAmount(r?.kWh, 'kWh', errors);
410
+ if (kWh === undefined && !errors.length)
411
+ errors.push('kWh 가 없다 — 이 문이 받는 값은 그 기간의 발전량이다');
412
+ const ACCUMULATIONS = ['lifetime', 'daily', 'monthly', 'billing', 'unknown'];
413
+ let accumulation;
414
+ const rawAcc = r?.accumulation;
415
+ if (rawAcc !== undefined && rawAcc !== null && String(rawAcc).trim()) {
416
+ const text = String(rawAcc).trim();
417
+ if (!ACCUMULATIONS.includes(text)) {
418
+ errors.push(`accumulation 이 아는 값이 아니다(${ACCUMULATIONS.join('·')}): ${JSON.stringify(rawAcc)}`);
419
+ }
420
+ else
421
+ accumulation = text;
422
+ }
423
+ /* 관측 구간은 기간 안에 있어야 한다 — 밖이면 그 값이 이 기간의 것이 아니다. */
424
+ const readAt = (raw, name) => {
425
+ if (raw === undefined || raw === null || !String(raw).trim())
426
+ return undefined;
427
+ const text = String(raw).trim();
428
+ if (!Number.isFinite(Date.parse(text))) {
429
+ errors.push(`${name} 를 시각으로 읽을 수 없다: ${JSON.stringify(raw)}`);
430
+ return undefined;
431
+ }
432
+ return text;
433
+ };
434
+ const observedFrom = readAt(r?.observedFrom, 'observedFrom');
435
+ const observedTo = readAt(r?.observedTo, 'observedTo');
436
+ if (errors.length || !span || kWh === undefined) {
437
+ rejected.push({ record: r, errors });
438
+ continue;
439
+ }
440
+ const data = {
441
+ equipmentId,
442
+ kWh,
443
+ periodStart: span.from,
444
+ periodEnd: span.to,
445
+ /* 관측 구간을 말하지 않으면 기간 전체를 잰 것으로 둔다 — 지난 기록은 그 원본이 하루를 마감해 준 값이다. */
446
+ observedFrom: observedFrom ?? span.from,
447
+ observedTo: observedTo ?? span.to,
448
+ accumulation: (accumulation ?? 'daily'),
449
+ /* 밖에서 마감되어 온 것이다 — 우리가 시각 기준으로 닫은 것이 아니다. */
450
+ boundary: 'declared'
451
+ };
452
+ accepted.push({
453
+ eventId: periodFactId(opts.tenantId, ENERGY_EVENT.generationPeriod, equipmentId, span.from, span.to),
454
+ eventType: ENERGY_EVENT.generationPeriod,
455
+ eventTime: span.to,
456
+ tenantId: opts.tenantId,
457
+ data
458
+ });
459
+ }
460
+ return { accepted, rejected };
461
+ }
381
462
  export function isEnergyGenerationPriceRecord(record) {
382
463
  const r = record;
383
464
  if (!r || typeof r !== 'object')
package/dist/index.d.ts CHANGED
@@ -30,10 +30,10 @@ 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 } from './energy-ingest.ts';
33
+ export { ingestEnergyRecords, isEnergyRecord, ingestEnergyEquipmentRecords, isEnergyEquipmentRecord, ingestEnergyGenerationRecords, isEnergyGenerationRecord, ingestEnergyUsagePeriodRecords, isEnergyUsagePeriodRecord, ingestEnergyBillRecords, isEnergyBillRecord, ingestEnergyTariffBasisRecords, isEnergyTariffBasisRecord, ingestEnergyGenerationPriceRecords, isEnergyGenerationPriceRecord, ingestEnergyGenerationPeriodRecords, isEnergyGenerationPeriodRecord } 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';
37
37
  export type { AttributionBasis, AttributionResult, ElectricityCost, EnergyConsumer, EnergyPool, EnergyShare, IntensityInput, IntensityResult, IntensityDenominator, TariffDeclaration, WeightKind, WindowedEnergy } from './energy-attribution.ts';
38
- export type { EnergyRecord, EnergyEquipmentRecord, EnergyGenerationRecord, EnergyUsagePeriodRecord, EnergyBillRecord, EnergyTariffBasisRecord, EnergyGenerationPriceRecord, EnergyIngestOptions, EnergyIngestResult } from './energy-ingest.ts';
38
+ export type { EnergyRecord, EnergyEquipmentRecord, EnergyGenerationRecord, EnergyUsagePeriodRecord, EnergyBillRecord, EnergyTariffBasisRecord, EnergyGenerationPriceRecord, EnergyGenerationPeriodRecord, EnergyIngestOptions, EnergyIngestResult } from './energy-ingest.ts';
39
39
  export * from './vocabulary.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 } from "./energy-ingest.js";
33
+ export { ingestEnergyRecords, isEnergyRecord, ingestEnergyEquipmentRecords, isEnergyEquipmentRecord, ingestEnergyGenerationRecords, isEnergyGenerationRecord, ingestEnergyUsagePeriodRecords, isEnergyUsagePeriodRecord, ingestEnergyBillRecords, isEnergyBillRecord, ingestEnergyTariffBasisRecords, isEnergyTariffBasisRecord, ingestEnergyGenerationPriceRecords, isEnergyGenerationPriceRecord, ingestEnergyGenerationPeriodRecords, isEnergyGenerationPeriodRecord } 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";
@@ -127,6 +127,7 @@ __export(index_exports, {
127
127
  ingest: () => ingest,
128
128
  ingestEnergyBillRecords: () => ingestEnergyBillRecords,
129
129
  ingestEnergyEquipmentRecords: () => ingestEnergyEquipmentRecords,
130
+ ingestEnergyGenerationPeriodRecords: () => ingestEnergyGenerationPeriodRecords,
130
131
  ingestEnergyGenerationPriceRecords: () => ingestEnergyGenerationPriceRecords,
131
132
  ingestEnergyGenerationRecords: () => ingestEnergyGenerationRecords,
132
133
  ingestEnergyRecords: () => ingestEnergyRecords,
@@ -138,6 +139,7 @@ __export(index_exports, {
138
139
  isElectricalLocationType: () => isElectricalLocationType,
139
140
  isEnergyBillRecord: () => isEnergyBillRecord,
140
141
  isEnergyEquipmentRecord: () => isEnergyEquipmentRecord,
142
+ isEnergyGenerationPeriodRecord: () => isEnergyGenerationPeriodRecord,
141
143
  isEnergyGenerationPriceRecord: () => isEnergyGenerationPriceRecord,
142
144
  isEnergyGenerationRecord: () => isEnergyGenerationRecord,
143
145
  isEnergyRecord: () => isEnergyRecord,
@@ -9970,6 +9972,7 @@ function ingestEnergyEquipmentRecords(records, opts) {
9970
9972
  return { accepted, rejected };
9971
9973
  }
9972
9974
  function isEnergyGenerationRecord(record) {
9975
+ if (record?.from !== void 0 || record?.to !== void 0) return false;
9973
9976
  if (!record || typeof record !== "object") return false;
9974
9977
  const r = record;
9975
9978
  return typeof r.equipmentId === "string" && r.equipmentId.trim().length > 0 && r.kWh !== void 0 && r.meterId === void 0 && r.epc === void 0;
@@ -10043,6 +10046,69 @@ function isEnergyUsagePeriodRecord(record) {
10043
10046
  const r = record;
10044
10047
  return !!r && typeof r === "object" && r.meterId !== void 0 && r.from !== void 0 && r.to !== void 0 && r.kWh !== void 0;
10045
10048
  }
10049
+ function isEnergyGenerationPeriodRecord(record) {
10050
+ const r = record;
10051
+ if (!r || typeof r !== "object") return false;
10052
+ if (r.equipmentId === void 0 || r.from === void 0 || r.to === void 0) return false;
10053
+ return r.kWh !== void 0;
10054
+ }
10055
+ function ingestEnergyGenerationPeriodRecords(records, opts) {
10056
+ const list = records === void 0 || records === null ? [] : Array.isArray(records) ? records : [records];
10057
+ const accepted = [];
10058
+ const rejected = [];
10059
+ for (const r of list) {
10060
+ const errors = [];
10061
+ const equipmentId = String(r?.equipmentId ?? "").trim();
10062
+ if (!equipmentId) errors.push("equipmentId \uAC00 \uC5C6\uB2E4 \u2014 \uC5B4\uB290 \uC124\uBE44\uAC00 \uB0B8 \uAC83\uC778\uC9C0 \uC9C0\uC5B4\uB0BC \uC218 \uC5C6\uB2E4");
10063
+ const span = readSpan(r, errors);
10064
+ const kWh = readAmount(r?.kWh, "kWh", errors);
10065
+ if (kWh === void 0 && !errors.length) errors.push("kWh \uAC00 \uC5C6\uB2E4 \u2014 \uC774 \uBB38\uC774 \uBC1B\uB294 \uAC12\uC740 \uADF8 \uAE30\uAC04\uC758 \uBC1C\uC804\uB7C9\uC774\uB2E4");
10066
+ const ACCUMULATIONS = ["lifetime", "daily", "monthly", "billing", "unknown"];
10067
+ let accumulation;
10068
+ const rawAcc = r?.accumulation;
10069
+ if (rawAcc !== void 0 && rawAcc !== null && String(rawAcc).trim()) {
10070
+ const text = String(rawAcc).trim();
10071
+ if (!ACCUMULATIONS.includes(text)) {
10072
+ errors.push(`accumulation \uC774 \uC544\uB294 \uAC12\uC774 \uC544\uB2C8\uB2E4(${ACCUMULATIONS.join("\xB7")}): ${JSON.stringify(rawAcc)}`);
10073
+ } else accumulation = text;
10074
+ }
10075
+ const readAt = (raw, name) => {
10076
+ if (raw === void 0 || raw === null || !String(raw).trim()) return void 0;
10077
+ const text = String(raw).trim();
10078
+ if (!Number.isFinite(Date.parse(text))) {
10079
+ errors.push(`${name} \uB97C \uC2DC\uAC01\uC73C\uB85C \uC77D\uC744 \uC218 \uC5C6\uB2E4: ${JSON.stringify(raw)}`);
10080
+ return void 0;
10081
+ }
10082
+ return text;
10083
+ };
10084
+ const observedFrom = readAt(r?.observedFrom, "observedFrom");
10085
+ const observedTo = readAt(r?.observedTo, "observedTo");
10086
+ if (errors.length || !span || kWh === void 0) {
10087
+ rejected.push({ record: r, errors });
10088
+ continue;
10089
+ }
10090
+ const data = {
10091
+ equipmentId,
10092
+ kWh,
10093
+ periodStart: span.from,
10094
+ periodEnd: span.to,
10095
+ /* 관측 구간을 말하지 않으면 기간 전체를 잰 것으로 둔다 — 지난 기록은 그 원본이 하루를 마감해 준 값이다. */
10096
+ observedFrom: observedFrom ?? span.from,
10097
+ observedTo: observedTo ?? span.to,
10098
+ accumulation: accumulation ?? "daily",
10099
+ /* 밖에서 마감되어 온 것이다 — 우리가 시각 기준으로 닫은 것이 아니다. */
10100
+ boundary: "declared"
10101
+ };
10102
+ accepted.push({
10103
+ eventId: periodFactId(opts.tenantId, ENERGY_EVENT.generationPeriod, equipmentId, span.from, span.to),
10104
+ eventType: ENERGY_EVENT.generationPeriod,
10105
+ eventTime: span.to,
10106
+ tenantId: opts.tenantId,
10107
+ data
10108
+ });
10109
+ }
10110
+ return { accepted, rejected };
10111
+ }
10046
10112
  function isEnergyGenerationPriceRecord(record) {
10047
10113
  const r = record;
10048
10114
  if (!r || typeof r !== "object") return false;
@@ -10938,6 +11004,7 @@ function retiredVocabularyIn(line) {
10938
11004
  ingest,
10939
11005
  ingestEnergyBillRecords,
10940
11006
  ingestEnergyEquipmentRecords,
11007
+ ingestEnergyGenerationPeriodRecords,
10941
11008
  ingestEnergyGenerationPriceRecords,
10942
11009
  ingestEnergyGenerationRecords,
10943
11010
  ingestEnergyRecords,
@@ -10949,6 +11016,7 @@ function retiredVocabularyIn(line) {
10949
11016
  isElectricalLocationType,
10950
11017
  isEnergyBillRecord,
10951
11018
  isEnergyEquipmentRecord,
11019
+ isEnergyGenerationPeriodRecord,
10952
11020
  isEnergyGenerationPriceRecord,
10953
11021
  isEnergyGenerationRecord,
10954
11022
  isEnergyRecord,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/twin-kernel",
3
- "version": "0.7.83",
3
+ "version": "0.7.84",
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": {