@operato/twin-kernel 0.7.1 → 0.7.2

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.
@@ -39,6 +39,14 @@ export declare class EmsKernel extends FlowEngine {
39
39
  * (기본값을 지어내면 그 뒤 모든 판정이 거짓 위에 선다).
40
40
  */
41
41
  private declaredContractKW;
42
+ /**
43
+ * **뿌리 계량기** — 조상 중에 계량된 자리가 없는 계량 지점들.
44
+ *
45
+ * 계층 계량(수전 ⊃ 분기)에서 전부 더하면 이중 계상이 된다. 뿌리만 더하면 현장의 실제 부하가 된다.
46
+ * 모델이 그 계량기의 자리를 모르면 뿌리로 본다(모르는 것을 빼면 부하가 조용히 작아진다 — 그것이
47
+ * 「계약 안쪽」이라는 더 위험한 거짓을 만든다).
48
+ */
49
+ private rootMeterIds;
42
50
  /**
43
51
  * 계측 표본을 받는다 — 에너지 사건만 가로채고 나머지는 그대로 상위에 넘긴다.
44
52
  *
@@ -93,6 +93,51 @@ export class EmsKernel extends FlowEngine {
93
93
  }
94
94
  return max;
95
95
  }
96
+ /**
97
+ * **뿌리 계량기** — 조상 중에 계량된 자리가 없는 계량 지점들.
98
+ *
99
+ * 계층 계량(수전 ⊃ 분기)에서 전부 더하면 이중 계상이 된다. 뿌리만 더하면 현장의 실제 부하가 된다.
100
+ * 모델이 그 계량기의 자리를 모르면 뿌리로 본다(모르는 것을 빼면 부하가 조용히 작아진다 — 그것이
101
+ * 「계약 안쪽」이라는 더 위험한 거짓을 만든다).
102
+ */
103
+ rootMeterIds() {
104
+ const locOf = new Map();
105
+ for (const e of this.boardDef?.equipment ?? [])
106
+ locOf.set(e.id, e.homeLocation);
107
+ const parentOf = new Map();
108
+ for (const l of this.boardDef?.locations ?? [])
109
+ parentOf.set(l.id, l.parentId);
110
+ /* 계량되고 있는 자리들 — 표본이 실제로 온 계량기의 자리만 센다(선언만 있고 값이 없는 계량기는
111
+ 부하에 기여하지 않으므로 계층 판정에서도 제외한다). */
112
+ const meteredLocs = new Set();
113
+ for (const id of this.points.keys()) {
114
+ const loc = locOf.get(id);
115
+ if (loc)
116
+ meteredLocs.add(loc);
117
+ }
118
+ const out = new Set();
119
+ for (const id of this.points.keys()) {
120
+ const loc = locOf.get(id);
121
+ if (!loc) {
122
+ out.add(id);
123
+ continue;
124
+ } // 자리를 모르면 뿌리로 본다
125
+ let cur = parentOf.get(loc);
126
+ let root = true;
127
+ const seen = new Set([loc]);
128
+ while (cur && !seen.has(cur)) {
129
+ if (meteredLocs.has(cur)) {
130
+ root = false;
131
+ break;
132
+ }
133
+ seen.add(cur);
134
+ cur = parentOf.get(cur);
135
+ }
136
+ if (root)
137
+ out.add(id);
138
+ }
139
+ return out;
140
+ }
96
141
  /**
97
142
  * 계측 표본을 받는다 — 에너지 사건만 가로채고 나머지는 그대로 상위에 넘긴다.
98
143
  *
@@ -138,12 +183,21 @@ export class EmsKernel extends FlowEngine {
138
183
  const w = this.open;
139
184
  if (kW !== undefined) {
140
185
  /*
141
- * 구간의 부하는 **지점의 합**이다(수전 하나만 계량하는 현장도, 피더를 여럿 계량하는 현장도 있다).
142
- * 마지막으로 관측된 값들을 더한다 — 표본 주기가 지점마다 달라도 이것이 그 순간의 최선이다.
143
- * 지점 하나가 침묵하면 값은 낡은 채로 더해진다: 그래서 `atMs` 지점마다 남겨 소비처가
144
- * 「이 지점은 3시간째 조용하다」를 있게 한다.
186
+ * ── 구간의 부하는 **뿌리 계량기의 합**이다 (2026-08-14 실측으로 고침) ────────
187
+ *
188
+ * 처음에는 「지점의 합」이라고만 했다. 그런데 실제 현장은 **계층 계량**이다: 수전 계량기가 공장
189
+ * 전체를 재고, 분기 계량기가 안의 라인을 나눠 잰다. 둘을 다 더하면 **이중 계상**이 되고,
190
+ * 목 데이터로 재 보니 실제 3,855kW 인 현장이 7,414kW 로 읽혀 **계약(4,000kW)을 넘었다는 거짓
191
+ * 판정**이 나왔다. 요금은 수전 지점에서 매겨지므로 그 값이 사실이다.
192
+ *
193
+ * 그래서 **뿌리만 더한다**: 자기 자리의 조상 중에 계량된 자리가 없는 계량기가 뿌리다. 수전만
194
+ * 계량하는 현장은 그것 하나, 분기만 계량하는 현장은 분기들의 합, 둘 다 계량하면 수전 하나.
195
+ *
196
+ * 모델이 그 계량기를 모르면(설비 선언에 없다) 뿌리로 본다 — 모르는 것을 빼면 그만큼 부하가
197
+ * 조용히 작아지고, 작아진 부하는 「계약 안쪽」이라는 더 위험한 거짓을 만든다.
145
198
  */
146
- const total = [...this.points.values()].reduce((sum, p) => sum + (p.kW ?? 0), 0);
199
+ const roots = this.rootMeterIds();
200
+ const total = [...this.points.values()].reduce((sum, p) => sum + (roots.has(p.id) ? p.kW ?? 0 : 0), 0);
147
201
  if (w.maxKW === undefined || total > w.maxKW)
148
202
  w.maxKW = total;
149
203
  w.samples++;
@@ -5628,6 +5628,45 @@ var EmsKernel = class extends FlowEngine {
5628
5628
  }
5629
5629
  return max;
5630
5630
  }
5631
+ /**
5632
+ * **뿌리 계량기** — 조상 중에 계량된 자리가 없는 계량 지점들.
5633
+ *
5634
+ * 계층 계량(수전 ⊃ 분기)에서 전부 더하면 이중 계상이 된다. 뿌리만 더하면 현장의 실제 부하가 된다.
5635
+ * 모델이 그 계량기의 자리를 모르면 뿌리로 본다(모르는 것을 빼면 부하가 조용히 작아진다 — 그것이
5636
+ * 「계약 안쪽」이라는 더 위험한 거짓을 만든다).
5637
+ */
5638
+ rootMeterIds() {
5639
+ const locOf = /* @__PURE__ */ new Map();
5640
+ for (const e of this.boardDef?.equipment ?? []) locOf.set(e.id, e.homeLocation);
5641
+ const parentOf = /* @__PURE__ */ new Map();
5642
+ for (const l of this.boardDef?.locations ?? []) parentOf.set(l.id, l.parentId);
5643
+ const meteredLocs = /* @__PURE__ */ new Set();
5644
+ for (const id of this.points.keys()) {
5645
+ const loc = locOf.get(id);
5646
+ if (loc) meteredLocs.add(loc);
5647
+ }
5648
+ const out = /* @__PURE__ */ new Set();
5649
+ for (const id of this.points.keys()) {
5650
+ const loc = locOf.get(id);
5651
+ if (!loc) {
5652
+ out.add(id);
5653
+ continue;
5654
+ }
5655
+ let cur = parentOf.get(loc);
5656
+ let root = true;
5657
+ const seen = /* @__PURE__ */ new Set([loc]);
5658
+ while (cur && !seen.has(cur)) {
5659
+ if (meteredLocs.has(cur)) {
5660
+ root = false;
5661
+ break;
5662
+ }
5663
+ seen.add(cur);
5664
+ cur = parentOf.get(cur);
5665
+ }
5666
+ if (root) out.add(id);
5667
+ }
5668
+ return out;
5669
+ }
5631
5670
  /**
5632
5671
  * 계측 표본을 받는다 — 에너지 사건만 가로채고 나머지는 그대로 상위에 넘긴다.
5633
5672
  *
@@ -5661,7 +5700,8 @@ var EmsKernel = class extends FlowEngine {
5661
5700
  this.points.set(id, point);
5662
5701
  const w = this.open;
5663
5702
  if (kW !== void 0) {
5664
- const total = [...this.points.values()].reduce((sum, p) => sum + (p.kW ?? 0), 0);
5703
+ const roots = this.rootMeterIds();
5704
+ const total = [...this.points.values()].reduce((sum, p) => sum + (roots.has(p.id) ? p.kW ?? 0 : 0), 0);
5665
5705
  if (w.maxKW === void 0 || total > w.maxKW) w.maxKW = total;
5666
5706
  w.samples++;
5667
5707
  w.meanKW = w.meanKW === void 0 ? total : (w.meanKW * (w.samples - 1) + total) / w.samples;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/twin-kernel",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
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": {