@operato/twin-kernel 0.7.1 → 0.7.3
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/capability.d.ts +10 -1
- package/dist/capability.js +17 -1
- package/dist/ems-kernel.d.ts +8 -0
- package/dist/ems-kernel.js +59 -5
- package/dist/ems-profile.js +6 -2
- package/dist-cjs/index.cjs +68 -4
- package/package.json +1 -1
package/dist/capability.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type CapabilityKey = 'operable' | 'storable' | 'mobile' | 'processable' | 'trackable' | 'metered' | 'curtailable' | 'generating' | 'storing';
|
|
1
|
+
export type CapabilityKey = 'operable' | 'storable' | 'mobile' | 'processable' | 'trackable' | 'switching' | 'metered' | 'curtailable' | 'generating' | 'storing';
|
|
2
2
|
/** 이동 관측 — Mobile 이 노출. */
|
|
3
3
|
export interface Motion {
|
|
4
4
|
fromNode: string;
|
|
@@ -58,6 +58,15 @@ export interface StoringState {
|
|
|
58
58
|
chargeKW?: number;
|
|
59
59
|
dischargeKW?: number;
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* 개폐 위치 — IEC 61850 `XCBR.Pos`·`XSWI.Pos` 의 넷 값을 그대로 쓴다.
|
|
63
|
+
*
|
|
64
|
+
* `intermediate`(과도)와 `bad`(위치 불량)를 **뭉개지 않는다**: 실 계통에서 이 둘은 이중 접점이 서로
|
|
65
|
+
* 다른 말을 하는 상태이고, 「열림/닫힘」으로 반올림하면 트윈이 없는 확신을 갖는다.
|
|
66
|
+
*/
|
|
67
|
+
export interface SwitchingState {
|
|
68
|
+
position?: 'open' | 'closed' | 'intermediate' | 'bad';
|
|
69
|
+
}
|
|
61
70
|
/** 능력 관측 계약 메타(발견·검증). ops 없음(원칙 ①). */
|
|
62
71
|
export interface CapabilitySpec {
|
|
63
72
|
key: CapabilityKey;
|
package/dist/capability.js
CHANGED
|
@@ -33,6 +33,22 @@ export const CAPABILITIES = {
|
|
|
33
33
|
key: 'processable', label: 'twin.capability.processable', semantics: '변환/가공 수행 — 산출(양품/불량). 운영 status 는 Operable 조합. progress 방출은 후속.',
|
|
34
34
|
stateFields: ['output'], results: ['completed']
|
|
35
35
|
},
|
|
36
|
+
/*
|
|
37
|
+
* ── 「위치를 읽는다」 ≠ 「조작할 수 있다」 (2026-08-14) ─────────────────────
|
|
38
|
+
* 차단기는 이 둘이 갈리는 자리다. 상태를 읽어 계통 구성을 알지만 **우리는 조작하지 않는다**
|
|
39
|
+
* (안전 계통은 범위 밖: profiles/ems.md §1). 그런데 능력을 `operable` 로 주면 저작면이 그 설비에
|
|
40
|
+
* **제어 컴포넌트를 권한다** — 화면이 우리가 하지 않기로 한 것을 하라고 부추긴다.
|
|
41
|
+
*
|
|
42
|
+
* 그래서 개폐 기기의 관측을 따로 세운다. 이름을 `observable` 로 하지 않은 이유: 그러면 상태 필드가
|
|
43
|
+
* `status` 여야 하고 그건 `operable` 의 것이다(원칙 ② 직교). 없는 필드를 지어내는 대신 **표준이 주는
|
|
44
|
+
* 이름**을 쓴다 — 개폐 기기의 관측은 `Pos`(위치)다.
|
|
45
|
+
*
|
|
46
|
+
* `operable` 은 그대로 「명령을 받는 능동 자원」의 자리로 남는다.
|
|
47
|
+
*/
|
|
48
|
+
switching: {
|
|
49
|
+
key: 'switching', label: 'twin.capability.switching', semantics: '회로를 열고 닫는 기기의 **위치를 읽는다** — 우리는 조작하지 않는다(명령 없음). IEC 61850 XCBR/XSWI Pos.',
|
|
50
|
+
stateFields: ['position'], models: ['SwitchingState'], results: ['positionChanged']
|
|
51
|
+
},
|
|
36
52
|
trackable: {
|
|
37
53
|
key: 'trackable', label: 'twin.capability.trackable', semantics: '오더/아이템 생애 추적 — 생애단계(도메인 라벨, 무방언)·진행·보류.',
|
|
38
54
|
stateFields: ['lifecycle', 'progress', 'held'], results: ['lifecycleChanged']
|
|
@@ -60,7 +76,7 @@ export const CAPABILITIES = {
|
|
|
60
76
|
stateFields: ['soc', 'chargeKW', 'dischargeKW'], models: ['StoringState'], invariants: ['0 <= soc <= 100'], results: ['stored', 'discharged']
|
|
61
77
|
}
|
|
62
78
|
};
|
|
63
|
-
export const CAPABILITY_KEYS = ['operable', 'storable', 'mobile', 'processable', 'trackable', 'metered', 'curtailable', 'generating', 'storing'];
|
|
79
|
+
export const CAPABILITY_KEYS = ['operable', 'storable', 'mobile', 'processable', 'trackable', 'switching', 'metered', 'curtailable', 'generating', 'storing'];
|
|
64
80
|
/** 능력 집합이 관측 노출하는 상태 필드 합집합(직교이므로 단순 병합). */
|
|
65
81
|
export function stateFieldsOf(caps) {
|
|
66
82
|
const out = new Set();
|
package/dist/ems-kernel.d.ts
CHANGED
|
@@ -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
|
*
|
package/dist/ems-kernel.js
CHANGED
|
@@ -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
|
-
*
|
|
144
|
-
*
|
|
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
|
|
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++;
|
package/dist/ems-profile.js
CHANGED
|
@@ -69,11 +69,15 @@ export const EMS_TYPES = [
|
|
|
69
69
|
label: 'twin.type.breaker',
|
|
70
70
|
/*
|
|
71
71
|
* 차단기 — IEC 61850 `XCBR`. **우리는 이것을 조작하지 않는다**(안전 계통은 범위 밖: ems.md §1).
|
|
72
|
-
* 상태를 읽어 계통 구성을 알
|
|
72
|
+
* 상태를 읽어 계통 구성을 알 뿐이다.
|
|
73
|
+
*
|
|
74
|
+
* 능력은 `switching` 이다 — `operable` 이 아니다(2026-08-14). `operable` 이던 동안 저작면이 차단기에
|
|
75
|
+
* **제어 컴포넌트**를 권했다: 우리가 하지 않기로 한 조작을 화면이 부추긴 것이다. 「위치를 읽는다」와
|
|
76
|
+
* 「명령을 받는다」는 다른 능력이고, 이제 그 둘이 갈려 있다(capability.ts 주석).
|
|
73
77
|
*/
|
|
74
78
|
standardClass: { iec61850: 'XCBR', iso55000: 'Asset' },
|
|
75
79
|
identity: { scheme: 'kernel:id' },
|
|
76
|
-
capabilities: ['
|
|
80
|
+
capabilities: ['switching']
|
|
77
81
|
},
|
|
78
82
|
{
|
|
79
83
|
key: 'pv-array',
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -1665,11 +1665,15 @@ var EMS_TYPES = [
|
|
|
1665
1665
|
label: "twin.type.breaker",
|
|
1666
1666
|
/*
|
|
1667
1667
|
* 차단기 — IEC 61850 `XCBR`. **우리는 이것을 조작하지 않는다**(안전 계통은 범위 밖: ems.md §1).
|
|
1668
|
-
* 상태를 읽어 계통 구성을 알
|
|
1668
|
+
* 상태를 읽어 계통 구성을 알 뿐이다.
|
|
1669
|
+
*
|
|
1670
|
+
* 능력은 `switching` 이다 — `operable` 이 아니다(2026-08-14). `operable` 이던 동안 저작면이 차단기에
|
|
1671
|
+
* **제어 컴포넌트**를 권했다: 우리가 하지 않기로 한 조작을 화면이 부추긴 것이다. 「위치를 읽는다」와
|
|
1672
|
+
* 「명령을 받는다」는 다른 능력이고, 이제 그 둘이 갈려 있다(capability.ts 주석).
|
|
1669
1673
|
*/
|
|
1670
1674
|
standardClass: { iec61850: "XCBR", iso55000: "Asset" },
|
|
1671
1675
|
identity: { scheme: "kernel:id" },
|
|
1672
|
-
capabilities: ["
|
|
1676
|
+
capabilities: ["switching"]
|
|
1673
1677
|
},
|
|
1674
1678
|
{
|
|
1675
1679
|
key: "pv-array",
|
|
@@ -1760,6 +1764,26 @@ var CAPABILITIES = {
|
|
|
1760
1764
|
stateFields: ["output"],
|
|
1761
1765
|
results: ["completed"]
|
|
1762
1766
|
},
|
|
1767
|
+
/*
|
|
1768
|
+
* ── 「위치를 읽는다」 ≠ 「조작할 수 있다」 (2026-08-14) ─────────────────────
|
|
1769
|
+
* 차단기는 이 둘이 갈리는 자리다. 상태를 읽어 계통 구성을 알지만 **우리는 조작하지 않는다**
|
|
1770
|
+
* (안전 계통은 범위 밖: profiles/ems.md §1). 그런데 능력을 `operable` 로 주면 저작면이 그 설비에
|
|
1771
|
+
* **제어 컴포넌트를 권한다** — 화면이 우리가 하지 않기로 한 것을 하라고 부추긴다.
|
|
1772
|
+
*
|
|
1773
|
+
* 그래서 개폐 기기의 관측을 따로 세운다. 이름을 `observable` 로 하지 않은 이유: 그러면 상태 필드가
|
|
1774
|
+
* `status` 여야 하고 그건 `operable` 의 것이다(원칙 ② 직교). 없는 필드를 지어내는 대신 **표준이 주는
|
|
1775
|
+
* 이름**을 쓴다 — 개폐 기기의 관측은 `Pos`(위치)다.
|
|
1776
|
+
*
|
|
1777
|
+
* `operable` 은 그대로 「명령을 받는 능동 자원」의 자리로 남는다.
|
|
1778
|
+
*/
|
|
1779
|
+
switching: {
|
|
1780
|
+
key: "switching",
|
|
1781
|
+
label: "twin.capability.switching",
|
|
1782
|
+
semantics: "\uD68C\uB85C\uB97C \uC5F4\uACE0 \uB2EB\uB294 \uAE30\uAE30\uC758 **\uC704\uCE58\uB97C \uC77D\uB294\uB2E4** \u2014 \uC6B0\uB9AC\uB294 \uC870\uC791\uD558\uC9C0 \uC54A\uB294\uB2E4(\uBA85\uB839 \uC5C6\uC74C). IEC 61850 XCBR/XSWI Pos.",
|
|
1783
|
+
stateFields: ["position"],
|
|
1784
|
+
models: ["SwitchingState"],
|
|
1785
|
+
results: ["positionChanged"]
|
|
1786
|
+
},
|
|
1763
1787
|
trackable: {
|
|
1764
1788
|
key: "trackable",
|
|
1765
1789
|
label: "twin.capability.trackable",
|
|
@@ -1808,7 +1832,7 @@ var CAPABILITIES = {
|
|
|
1808
1832
|
results: ["stored", "discharged"]
|
|
1809
1833
|
}
|
|
1810
1834
|
};
|
|
1811
|
-
var CAPABILITY_KEYS = ["operable", "storable", "mobile", "processable", "trackable", "metered", "curtailable", "generating", "storing"];
|
|
1835
|
+
var CAPABILITY_KEYS = ["operable", "storable", "mobile", "processable", "trackable", "switching", "metered", "curtailable", "generating", "storing"];
|
|
1812
1836
|
function stateFieldsOf(caps) {
|
|
1813
1837
|
const out = /* @__PURE__ */ new Set();
|
|
1814
1838
|
for (const c of caps) for (const f of CAPABILITIES[c]?.stateFields ?? []) out.add(f);
|
|
@@ -5628,6 +5652,45 @@ var EmsKernel = class extends FlowEngine {
|
|
|
5628
5652
|
}
|
|
5629
5653
|
return max;
|
|
5630
5654
|
}
|
|
5655
|
+
/**
|
|
5656
|
+
* **뿌리 계량기** — 조상 중에 계량된 자리가 없는 계량 지점들.
|
|
5657
|
+
*
|
|
5658
|
+
* 계층 계량(수전 ⊃ 분기)에서 전부 더하면 이중 계상이 된다. 뿌리만 더하면 현장의 실제 부하가 된다.
|
|
5659
|
+
* 모델이 그 계량기의 자리를 모르면 뿌리로 본다(모르는 것을 빼면 부하가 조용히 작아진다 — 그것이
|
|
5660
|
+
* 「계약 안쪽」이라는 더 위험한 거짓을 만든다).
|
|
5661
|
+
*/
|
|
5662
|
+
rootMeterIds() {
|
|
5663
|
+
const locOf = /* @__PURE__ */ new Map();
|
|
5664
|
+
for (const e of this.boardDef?.equipment ?? []) locOf.set(e.id, e.homeLocation);
|
|
5665
|
+
const parentOf = /* @__PURE__ */ new Map();
|
|
5666
|
+
for (const l of this.boardDef?.locations ?? []) parentOf.set(l.id, l.parentId);
|
|
5667
|
+
const meteredLocs = /* @__PURE__ */ new Set();
|
|
5668
|
+
for (const id of this.points.keys()) {
|
|
5669
|
+
const loc = locOf.get(id);
|
|
5670
|
+
if (loc) meteredLocs.add(loc);
|
|
5671
|
+
}
|
|
5672
|
+
const out = /* @__PURE__ */ new Set();
|
|
5673
|
+
for (const id of this.points.keys()) {
|
|
5674
|
+
const loc = locOf.get(id);
|
|
5675
|
+
if (!loc) {
|
|
5676
|
+
out.add(id);
|
|
5677
|
+
continue;
|
|
5678
|
+
}
|
|
5679
|
+
let cur = parentOf.get(loc);
|
|
5680
|
+
let root = true;
|
|
5681
|
+
const seen = /* @__PURE__ */ new Set([loc]);
|
|
5682
|
+
while (cur && !seen.has(cur)) {
|
|
5683
|
+
if (meteredLocs.has(cur)) {
|
|
5684
|
+
root = false;
|
|
5685
|
+
break;
|
|
5686
|
+
}
|
|
5687
|
+
seen.add(cur);
|
|
5688
|
+
cur = parentOf.get(cur);
|
|
5689
|
+
}
|
|
5690
|
+
if (root) out.add(id);
|
|
5691
|
+
}
|
|
5692
|
+
return out;
|
|
5693
|
+
}
|
|
5631
5694
|
/**
|
|
5632
5695
|
* 계측 표본을 받는다 — 에너지 사건만 가로채고 나머지는 그대로 상위에 넘긴다.
|
|
5633
5696
|
*
|
|
@@ -5661,7 +5724,8 @@ var EmsKernel = class extends FlowEngine {
|
|
|
5661
5724
|
this.points.set(id, point);
|
|
5662
5725
|
const w = this.open;
|
|
5663
5726
|
if (kW !== void 0) {
|
|
5664
|
-
const
|
|
5727
|
+
const roots = this.rootMeterIds();
|
|
5728
|
+
const total = [...this.points.values()].reduce((sum, p) => sum + (roots.has(p.id) ? p.kW ?? 0 : 0), 0);
|
|
5665
5729
|
if (w.maxKW === void 0 || total > w.maxKW) w.maxKW = total;
|
|
5666
5730
|
w.samples++;
|
|
5667
5731
|
w.meanKW = w.meanKW === void 0 ? total : (w.meanKW * (w.samples - 1) + total) / w.samples;
|
package/package.json
CHANGED