@operato/twin-kernel 0.7.7 → 0.7.8

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.
@@ -1,4 +1,4 @@
1
- export type CapabilityKey = 'operable' | 'storable' | 'mobile' | 'processable' | 'trackable' | 'switching' | 'metered' | 'curtailable' | 'generating' | 'storing';
1
+ export type CapabilityKey = 'operable' | 'storable' | 'mobile' | 'processable' | 'trackable' | 'switching' | 'metered' | 'curtailable' | 'energyGenerating' | 'energyStoring';
2
2
  /** 이동 관측 — Mobile 이 노출. */
3
3
  export interface Motion {
4
4
  fromNode: string;
@@ -74,6 +74,28 @@ export interface CapabilitySpec {
74
74
  semantics: string;
75
75
  /** 관측 상태 필드(데이터 형식). 직교 — 어느 필드도 두 능력에 중복 없음(원칙 ②). */
76
76
  stateFields: string[];
77
+ /**
78
+ * 이 능력을 가진 자원이 **받는 명령** — 나가는 사실(`results`)의 짝.
79
+ *
80
+ * ── 계약이 반쪽이었다 (2026-08-15) ────────────────────────────────────────
81
+ * 능력은 「무슨 값을 내보내는가」만 선언하고 있었다. 그런데 저작면이 알아야 할 것이 하나 더 있다:
82
+ * 「여기에 제어를 붙여도 되는가」. 적을 자리가 없어서 **능력 이름으로 눈치껏** 판단했다 —
83
+ * `operable` 이면 제어를 권하고 아니면 안 권했다.
84
+ *
85
+ * 차단기에서 그 방식이 막혔다. 상태는 보여주고 싶은데 `operable` 을 주면 화면이 우리가 하지 않기로
86
+ * 한 조작을 권한다. 그래서 능력을 새로 만들었고(`switching`), 능력은 상태 필드로 정의되므로 새
87
+ * 필드가 필요했는데 마침 표준이 `position` 이라는 이름을 줘서 됐다 — **그건 운이었다.**
88
+ *
89
+ * 없던 것은 참/거짓 하나가 아니라 **대칭**이다: 나가는 것은 선언돼 있고 들어오는 것은 없었다.
90
+ * 명령 어휘(`CMD`)는 이미 능력별로 묶여 있었지만 그 묶임이 **주석에만** 있었다 — 여기로 옮긴다.
91
+ *
92
+ * 비어 있다는 것이 곧 「조작하지 않는다」이다. 「명령을 받나」는 이 목록에서 **파생**된다 —
93
+ * 그래서 형용사를 새로 만들지 않는다.
94
+ *
95
+ * 트윈 수준 명령(`attention.ack`·`resource.add`)은 특정 자원의 능력이 아니므로 여기 넣지 않는다.
96
+ * 모든 명령이 능력에 속해야 한다는 규칙을 만들면 그것이 다음 땜빵이 된다.
97
+ */
98
+ commands: string[];
77
99
  /** 교환 데이터 모델 이름. */
78
100
  models?: string[];
79
101
  /** 불변식(모두 준수·신뢰). */
@@ -85,3 +107,10 @@ export declare const CAPABILITIES: Record<CapabilityKey, CapabilitySpec>;
85
107
  export declare const CAPABILITY_KEYS: CapabilityKey[];
86
108
  /** 능력 집합이 관측 노출하는 상태 필드 합집합(직교이므로 단순 병합). */
87
109
  export declare function stateFieldsOf(caps: CapabilityKey[]): string[];
110
+ /**
111
+ * 능력 집합이 **받는 명령**의 합집합 — `stateFieldsOf` 의 짝.
112
+ *
113
+ * 「이 자원에 제어를 붙여도 되는가」는 이 목록이 비었는지로 **파생**한다. 그래서 `controllable` 같은
114
+ * 형용사를 따로 두지 않는다 — 형용사를 두면 목록과 형용사가 언젠가 어긋나고, 어긋난 쪽이 화면이 된다.
115
+ */
116
+ export declare function commandsOf(caps: CapabilityKey[]): string[];
@@ -19,18 +19,22 @@
19
19
  export const CAPABILITIES = {
20
20
  operable: {
21
21
  key: 'operable', label: 'twin.capability.operable', semantics: '능동 자원의 운영 상태(유휴/가동/고장). status 교차 관심사를 여기 하나로.',
22
+ commands: ['resource.hold', 'resource.resume', 'resource.down', 'resource.repair', 'resource.reset-metrics'],
22
23
  stateFields: ['status'], results: ['statusChanged']
23
24
  },
24
25
  storable: {
25
26
  key: 'storable', label: 'twin.capability.storable', semantics: '아이템을 보유하는 위치 — 점유/용량. (씬 기제: Capacity)',
27
+ commands: [],
26
28
  stateFields: ['occupancy', 'capacity'], invariants: ['0 <= occupancy <= capacity (capacity>0)'], results: ['occupancyChanged']
27
29
  },
28
30
  mobile: {
29
31
  key: 'mobile', label: 'twin.capability.mobile', semantics: '자원 자신이 자리 간 이동. Transferable(아이템 이동)과 다름. (씬 기제: CarrierLine)',
32
+ commands: [],
30
33
  stateFields: ['location', 'motion'], models: ['Motion'], results: ['moved', 'motionTick']
31
34
  },
32
35
  processable: {
33
36
  key: 'processable', label: 'twin.capability.processable', semantics: '변환/가공 수행 — 산출(양품/불량). 운영 status 는 Operable 조합. progress 방출은 후속.',
37
+ commands: [],
34
38
  stateFields: ['output'], results: ['completed']
35
39
  },
36
40
  /*
@@ -47,10 +51,12 @@ export const CAPABILITIES = {
47
51
  */
48
52
  switching: {
49
53
  key: 'switching', label: 'twin.capability.switching', semantics: '회로를 열고 닫는 기기의 **위치를 읽는다** — 우리는 조작하지 않는다(명령 없음). IEC 61850 XCBR/XSWI Pos.',
54
+ commands: [],
50
55
  stateFields: ['position'], models: ['SwitchingState'], results: ['positionChanged']
51
56
  },
52
57
  trackable: {
53
58
  key: 'trackable', label: 'twin.capability.trackable', semantics: '오더/아이템 생애 추적 — 생애단계(도메인 라벨, 무방언)·진행·보류.',
59
+ commands: ['order.hold', 'order.resume', 'order.release'],
54
60
  stateFields: ['lifecycle', 'progress', 'held'], results: ['lifecycleChanged']
55
61
  },
56
62
  /*
@@ -61,22 +67,35 @@ export const CAPABILITIES = {
61
67
  */
62
68
  metered: {
63
69
  key: 'metered', label: 'twin.capability.metered', semantics: '자기 소비를 잰다 — 유효전력·누적량·역률. 값이 없으면 모르는 것이다(0 이 아니다).',
70
+ commands: [],
64
71
  stateFields: ['kW', 'kWh', 'powerFactor'], models: ['MeteredState'], results: ['measured']
65
72
  },
66
73
  curtailable: {
67
74
  key: 'curtailable', label: 'twin.capability.curtailable', semantics: '줄일 수 있다 — 최소 유지 아래로는 내리지 않는다. 판정만 하고 집행은 사람과 그 시스템의 몫이다.',
75
+ commands: [],
68
76
  stateFields: ['curtailable', 'minKW'], models: ['CurtailableState'], invariants: ['minKW >= 0'], results: ['drSuggested']
69
77
  },
70
- generating: {
71
- key: 'generating', label: 'twin.capability.generating', semantics: '전기를 만든다 — 역송은 소비의 음수가 아니라 별개 사실이다.',
78
+ energyGenerating: {
79
+ key: 'energyGenerating', label: 'twin.capability.energyGenerating', semantics: '전기를 만든다 — 역송은 소비의 음수가 아니라 별개 사실이다.',
80
+ commands: [],
72
81
  stateFields: ['generatedKW', 'exportKW'], models: ['GeneratingState'], results: ['generated']
73
82
  },
74
- storing: {
75
- key: 'storing', label: 'twin.capability.storing', semantics: '담고 낸다 충전과 방전을 나눈다(손실·수명 판단이 둘을 구별해야 한다).',
83
+ /*
84
+ * ── `storing` 아니라 `energyStoring` 인가 (2026-08-15) ────────────────
85
+ * 같은 카탈로그에 `storable`(물건을 담는 자리)이 있다. 전기를 담는 것과 물건을 담는 것이 한 글자
86
+ * 차이로 붙어 있어서, 컴포넌트를 만들 때 `twin-storage` 를 피해 `twin-storage-energy` 로 도망쳤다 —
87
+ * 한 이름을 두 번 피해 갔다면 그 이름이 틀린 것이다.
88
+ *
89
+ * 도메인(EMS)이 아니라 **양(에너지)** 을 이름에 넣는다. 그래서 MES 트윈 안의 디젤 발전기도 그대로
90
+ * `energyGenerating` 이다 — 능력은 도메인이 아니라 관측의 면이다(방언 없음).
91
+ */
92
+ energyStoring: {
93
+ key: 'energyStoring', label: 'twin.capability.energyStoring', semantics: '담고 낸다 — 충전과 방전을 나눈다(손실·수명 판단이 둘을 구별해야 한다).',
94
+ commands: [],
76
95
  stateFields: ['soc', 'chargeKW', 'dischargeKW'], models: ['StoringState'], invariants: ['0 <= soc <= 100'], results: ['stored', 'discharged']
77
96
  }
78
97
  };
79
- export const CAPABILITY_KEYS = ['operable', 'storable', 'mobile', 'processable', 'trackable', 'switching', 'metered', 'curtailable', 'generating', 'storing'];
98
+ export const CAPABILITY_KEYS = ['operable', 'storable', 'mobile', 'processable', 'trackable', 'switching', 'metered', 'curtailable', 'energyGenerating', 'energyStoring'];
80
99
  /** 능력 집합이 관측 노출하는 상태 필드 합집합(직교이므로 단순 병합). */
81
100
  export function stateFieldsOf(caps) {
82
101
  const out = new Set();
@@ -85,3 +104,16 @@ export function stateFieldsOf(caps) {
85
104
  out.add(f);
86
105
  return [...out];
87
106
  }
107
+ /**
108
+ * 능력 집합이 **받는 명령**의 합집합 — `stateFieldsOf` 의 짝.
109
+ *
110
+ * 「이 자원에 제어를 붙여도 되는가」는 이 목록이 비었는지로 **파생**한다. 그래서 `controllable` 같은
111
+ * 형용사를 따로 두지 않는다 — 형용사를 두면 목록과 형용사가 언젠가 어긋나고, 어긋난 쪽이 화면이 된다.
112
+ */
113
+ export function commandsOf(caps) {
114
+ const out = new Set();
115
+ for (const c of caps)
116
+ for (const cmd of CAPABILITIES[c]?.commands ?? [])
117
+ out.add(cmd);
118
+ return [...out];
119
+ }
@@ -96,7 +96,7 @@ export const EMS_TYPES = [
96
96
  /* 태양광 어레이 — IEC 61850-7-420(분산자원)의 `DPVA`. 발전과 계량은 다른 능력이다. */
97
97
  standardClass: { iec61850: 'DPVA', iso55000: 'Asset', iso50001: 'RenewableSupply' },
98
98
  identity: { scheme: 'kernel:id' },
99
- capabilities: ['generating', 'metered', 'operable']
99
+ capabilities: ['energyGenerating', 'metered', 'operable']
100
100
  },
101
101
  {
102
102
  key: 'battery',
@@ -105,7 +105,7 @@ export const EMS_TYPES = [
105
105
  /* 축전지(ESS) — `ZBAT`. 충전·방전을 나눠 재고, 저장은 보관(`storable`)이 아니다(물건이 아니다). */
106
106
  standardClass: { iec61850: 'ZBAT', iso55000: 'Asset' },
107
107
  identity: { scheme: 'kernel:id' },
108
- capabilities: ['storing', 'metered', 'operable']
108
+ capabilities: ['energyStoring', 'metered', 'operable']
109
109
  },
110
110
  {
111
111
  key: 'utility',
@@ -85,6 +85,7 @@ __export(index_exports, {
85
85
  capabilitiesForType: () => capabilitiesForType,
86
86
  capabilityOf: () => capabilityOf,
87
87
  classClosure: () => classClosure,
88
+ commandsOf: () => commandsOf,
88
89
  compareStates: () => compareStates,
89
90
  computeOee: () => computeOee,
90
91
  constantDuration: () => constantDuration,
@@ -1700,7 +1701,7 @@ var EMS_TYPES = [
1700
1701
  /* 태양광 어레이 — IEC 61850-7-420(분산자원)의 `DPVA`. 발전과 계량은 다른 능력이다. */
1701
1702
  standardClass: { iec61850: "DPVA", iso55000: "Asset", iso50001: "RenewableSupply" },
1702
1703
  identity: { scheme: "kernel:id" },
1703
- capabilities: ["generating", "metered", "operable"]
1704
+ capabilities: ["energyGenerating", "metered", "operable"]
1704
1705
  },
1705
1706
  {
1706
1707
  key: "battery",
@@ -1709,7 +1710,7 @@ var EMS_TYPES = [
1709
1710
  /* 축전지(ESS) — `ZBAT`. 충전·방전을 나눠 재고, 저장은 보관(`storable`)이 아니다(물건이 아니다). */
1710
1711
  standardClass: { iec61850: "ZBAT", iso55000: "Asset" },
1711
1712
  identity: { scheme: "kernel:id" },
1712
- capabilities: ["storing", "metered", "operable"]
1713
+ capabilities: ["energyStoring", "metered", "operable"]
1713
1714
  },
1714
1715
  {
1715
1716
  key: "utility",
@@ -1756,6 +1757,7 @@ var CAPABILITIES = {
1756
1757
  key: "operable",
1757
1758
  label: "twin.capability.operable",
1758
1759
  semantics: "\uB2A5\uB3D9 \uC790\uC6D0\uC758 \uC6B4\uC601 \uC0C1\uD0DC(\uC720\uD734/\uAC00\uB3D9/\uACE0\uC7A5). status \uAD50\uCC28 \uAD00\uC2EC\uC0AC\uB97C \uC5EC\uAE30 \uD558\uB098\uB85C.",
1760
+ commands: ["resource.hold", "resource.resume", "resource.down", "resource.repair", "resource.reset-metrics"],
1759
1761
  stateFields: ["status"],
1760
1762
  results: ["statusChanged"]
1761
1763
  },
@@ -1763,6 +1765,7 @@ var CAPABILITIES = {
1763
1765
  key: "storable",
1764
1766
  label: "twin.capability.storable",
1765
1767
  semantics: "\uC544\uC774\uD15C\uC744 \uBCF4\uC720\uD558\uB294 \uC704\uCE58 \u2014 \uC810\uC720/\uC6A9\uB7C9. (\uC52C \uAE30\uC81C: Capacity)",
1768
+ commands: [],
1766
1769
  stateFields: ["occupancy", "capacity"],
1767
1770
  invariants: ["0 <= occupancy <= capacity (capacity>0)"],
1768
1771
  results: ["occupancyChanged"]
@@ -1771,6 +1774,7 @@ var CAPABILITIES = {
1771
1774
  key: "mobile",
1772
1775
  label: "twin.capability.mobile",
1773
1776
  semantics: "\uC790\uC6D0 \uC790\uC2E0\uC774 \uC790\uB9AC \uAC04 \uC774\uB3D9. Transferable(\uC544\uC774\uD15C \uC774\uB3D9)\uACFC \uB2E4\uB984. (\uC52C \uAE30\uC81C: CarrierLine)",
1777
+ commands: [],
1774
1778
  stateFields: ["location", "motion"],
1775
1779
  models: ["Motion"],
1776
1780
  results: ["moved", "motionTick"]
@@ -1779,6 +1783,7 @@ var CAPABILITIES = {
1779
1783
  key: "processable",
1780
1784
  label: "twin.capability.processable",
1781
1785
  semantics: "\uBCC0\uD658/\uAC00\uACF5 \uC218\uD589 \u2014 \uC0B0\uCD9C(\uC591\uD488/\uBD88\uB7C9). \uC6B4\uC601 status \uB294 Operable \uC870\uD569. progress \uBC29\uCD9C\uC740 \uD6C4\uC18D.",
1786
+ commands: [],
1782
1787
  stateFields: ["output"],
1783
1788
  results: ["completed"]
1784
1789
  },
@@ -1798,6 +1803,7 @@ var CAPABILITIES = {
1798
1803
  key: "switching",
1799
1804
  label: "twin.capability.switching",
1800
1805
  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.",
1806
+ commands: [],
1801
1807
  stateFields: ["position"],
1802
1808
  models: ["SwitchingState"],
1803
1809
  results: ["positionChanged"]
@@ -1806,6 +1812,7 @@ var CAPABILITIES = {
1806
1812
  key: "trackable",
1807
1813
  label: "twin.capability.trackable",
1808
1814
  semantics: "\uC624\uB354/\uC544\uC774\uD15C \uC0DD\uC560 \uCD94\uC801 \u2014 \uC0DD\uC560\uB2E8\uACC4(\uB3C4\uBA54\uC778 \uB77C\uBCA8, \uBB34\uBC29\uC5B8)\xB7\uC9C4\uD589\xB7\uBCF4\uB958.",
1815
+ commands: ["order.hold", "order.resume", "order.release"],
1809
1816
  stateFields: ["lifecycle", "progress", "held"],
1810
1817
  results: ["lifecycleChanged"]
1811
1818
  },
@@ -1819,6 +1826,7 @@ var CAPABILITIES = {
1819
1826
  key: "metered",
1820
1827
  label: "twin.capability.metered",
1821
1828
  semantics: "\uC790\uAE30 \uC18C\uBE44\uB97C \uC7B0\uB2E4 \u2014 \uC720\uD6A8\uC804\uB825\xB7\uB204\uC801\uB7C9\xB7\uC5ED\uB960. \uAC12\uC774 \uC5C6\uC73C\uBA74 \uBAA8\uB974\uB294 \uAC83\uC774\uB2E4(0 \uC774 \uC544\uB2C8\uB2E4).",
1829
+ commands: [],
1822
1830
  stateFields: ["kW", "kWh", "powerFactor"],
1823
1831
  models: ["MeteredState"],
1824
1832
  results: ["measured"]
@@ -1827,35 +1835,52 @@ var CAPABILITIES = {
1827
1835
  key: "curtailable",
1828
1836
  label: "twin.capability.curtailable",
1829
1837
  semantics: "\uC904\uC77C \uC218 \uC788\uB2E4 \u2014 \uCD5C\uC18C \uC720\uC9C0 \uC544\uB798\uB85C\uB294 \uB0B4\uB9AC\uC9C0 \uC54A\uB294\uB2E4. \uD310\uC815\uB9CC \uD558\uACE0 \uC9D1\uD589\uC740 \uC0AC\uB78C\uACFC \uADF8 \uC2DC\uC2A4\uD15C\uC758 \uBAAB\uC774\uB2E4.",
1838
+ commands: [],
1830
1839
  stateFields: ["curtailable", "minKW"],
1831
1840
  models: ["CurtailableState"],
1832
1841
  invariants: ["minKW >= 0"],
1833
1842
  results: ["drSuggested"]
1834
1843
  },
1835
- generating: {
1836
- key: "generating",
1837
- label: "twin.capability.generating",
1844
+ energyGenerating: {
1845
+ key: "energyGenerating",
1846
+ label: "twin.capability.energyGenerating",
1838
1847
  semantics: "\uC804\uAE30\uB97C \uB9CC\uB4E0\uB2E4 \u2014 \uC5ED\uC1A1\uC740 \uC18C\uBE44\uC758 \uC74C\uC218\uAC00 \uC544\uB2C8\uB77C \uBCC4\uAC1C \uC0AC\uC2E4\uC774\uB2E4.",
1848
+ commands: [],
1839
1849
  stateFields: ["generatedKW", "exportKW"],
1840
1850
  models: ["GeneratingState"],
1841
1851
  results: ["generated"]
1842
1852
  },
1843
- storing: {
1844
- key: "storing",
1845
- label: "twin.capability.storing",
1853
+ /*
1854
+ * ── 왜 `storing` 이 아니라 `energyStoring` 인가 (2026-08-15) ────────────────
1855
+ * 같은 카탈로그에 `storable`(물건을 담는 자리)이 있다. 전기를 담는 것과 물건을 담는 것이 한 글자
1856
+ * 차이로 붙어 있어서, 컴포넌트를 만들 때 `twin-storage` 를 피해 `twin-storage-energy` 로 도망쳤다 —
1857
+ * 한 이름을 두 번 피해 갔다면 그 이름이 틀린 것이다.
1858
+ *
1859
+ * 도메인(EMS)이 아니라 **양(에너지)** 을 이름에 넣는다. 그래서 MES 트윈 안의 디젤 발전기도 그대로
1860
+ * `energyGenerating` 이다 — 능력은 도메인이 아니라 관측의 면이다(방언 없음).
1861
+ */
1862
+ energyStoring: {
1863
+ key: "energyStoring",
1864
+ label: "twin.capability.energyStoring",
1846
1865
  semantics: "\uB2F4\uACE0 \uB0B8\uB2E4 \u2014 \uCDA9\uC804\uACFC \uBC29\uC804\uC744 \uB098\uB208\uB2E4(\uC190\uC2E4\xB7\uC218\uBA85 \uD310\uB2E8\uC774 \uB458\uC744 \uAD6C\uBCC4\uD574\uC57C \uD55C\uB2E4).",
1866
+ commands: [],
1847
1867
  stateFields: ["soc", "chargeKW", "dischargeKW"],
1848
1868
  models: ["StoringState"],
1849
1869
  invariants: ["0 <= soc <= 100"],
1850
1870
  results: ["stored", "discharged"]
1851
1871
  }
1852
1872
  };
1853
- var CAPABILITY_KEYS = ["operable", "storable", "mobile", "processable", "trackable", "switching", "metered", "curtailable", "generating", "storing"];
1873
+ var CAPABILITY_KEYS = ["operable", "storable", "mobile", "processable", "trackable", "switching", "metered", "curtailable", "energyGenerating", "energyStoring"];
1854
1874
  function stateFieldsOf(caps) {
1855
1875
  const out = /* @__PURE__ */ new Set();
1856
1876
  for (const c of caps) for (const f of CAPABILITIES[c]?.stateFields ?? []) out.add(f);
1857
1877
  return [...out];
1858
1878
  }
1879
+ function commandsOf(caps) {
1880
+ const out = /* @__PURE__ */ new Set();
1881
+ for (const c of caps) for (const cmd of CAPABILITIES[c]?.commands ?? []) out.add(cmd);
1882
+ return [...out];
1883
+ }
1859
1884
 
1860
1885
  // src/yms-profile.ts
1861
1886
  var YARD_BIZSTEP = {
@@ -6446,6 +6471,7 @@ function retiredVocabularyIn(line) {
6446
6471
  capabilitiesForType,
6447
6472
  capabilityOf,
6448
6473
  classClosure,
6474
+ commandsOf,
6449
6475
  compareStates,
6450
6476
  computeOee,
6451
6477
  constantDuration,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/twin-kernel",
3
- "version": "0.7.7",
3
+ "version": "0.7.8",
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": {