@operato/twin-kernel 0.11.26 → 0.11.27

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.
@@ -1044,7 +1044,26 @@ var BIZSTEP = {
1044
1044
  packing: "urn:epcglobal:cbv:bizstep:packing",
1045
1045
  staging_outbound: "urn:epcglobal:cbv:bizstep:staging_outbound",
1046
1046
  shipping: "urn:epcglobal:cbv:bizstep:shipping",
1047
- replenishing: "urn:epcglobal:cbv:bizstep:replenishing"
1047
+ /**
1048
+ * **Replenishment** — moving goods to where they are picked from. CBV has no `replenishing`; this contract
1049
+ * wrote one until 2026-09-24, and the closed-list test found it. CBV `stocking` is the step that means it:
1050
+ * "…within a location to make an object available to the customer or for order fulfilment within a DC"
1051
+ * (CBV ontology, `BizStep-stocking`). Ruled by the chief architect; no reader branches on the value, and
1052
+ * the journal keeps what it already wrote (ADR-0038).
1053
+ */
1054
+ stocking: "urn:epcglobal:cbv:bizstep:stocking",
1055
+ /**
1056
+ * **Counted, and the book set to the count** — CBV `cycle_counting`. Primary source: the CBV ontology
1057
+ * (gs1/EPCIS `Ontology/CBV.ttl`, `cbv:BizStep-cycle_counting`, also served at ref.gs1.org/cbv): "Process of
1058
+ * counting objects within a location in order to obtain an accurate inventory for business needs other
1059
+ * than accounting purposes (e.g., replenishment and allocation)."
1060
+ *
1061
+ * **Only for an event somebody actually counted** (chief architect, 2026-09-24). A person correcting the
1062
+ * book without counting is `CBV_BIZSTEP.other` with the reason carried as a value — `epcis.ts` says why:
1063
+ * writing that as `cycle_counting` records a count that never happened. A count for the accounts is a
1064
+ * different CBV step (`stock_taking`) and is not this one.
1065
+ */
1066
+ cycle_counting: "urn:epcglobal:cbv:bizstep:cycle_counting"
1048
1067
  };
1049
1068
  var BTT = {
1050
1069
  /** Purchase Order — the document an inbound receipt is made against. */
@@ -1092,634 +1111,212 @@ var WMS_TYPES = [
1092
1111
  { key: "packer", role: "equipment", label: "twin.type.packer", standardClass: { epcis: "object", isa95: "Equipment" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] }
1093
1112
  ];
1094
1113
 
1095
- // ../ops-contract/dist/yms-profile.js
1096
- var YARD_BIZSTEP = {
1097
- arriving: "urn:epcglobal:cbv:bizstep:arriving",
1098
- // 게이트-인
1099
- staging: "urn:epcglobal:cbv:bizstep:staging",
1100
- // 야드 슬롯 주차·대기
1101
- loading: "urn:epcglobal:cbv:bizstep:loading",
1102
- // 도크 작업(상차: 트레일러←화물)
1103
- unloading: "urn:epcglobal:cbv:bizstep:unloading",
1104
- // 도크 작업(하차: 트레일러→화물)
1105
- departing: "urn:epcglobal:cbv:bizstep:departing"
1106
- // 게이트-아웃
1107
- };
1108
- var YMS_LOCATION_TYPES = ["gate", "yard-slot", "dock-door", "staging"];
1109
- var YMS_LOCATION_LEVEL = {
1110
- gate: "Other",
1111
- "yard-slot": "StorageUnit",
1112
- "dock-door": "StorageUnit",
1113
- staging: "StorageZone"
1114
- };
1115
- var YMS_TYPES = [
1116
- ...YMS_LOCATION_TYPES.map((k) => ({ key: k, role: "location", label: `twin.type.${k}`, standardClass: { epcis: "bizLocation" }, identity: { scheme: "gs1:SGLN" }, level: YMS_LOCATION_LEVEL[k], capabilities: ["storable"] })),
1117
- { key: "hostler", role: "equipment", label: "twin.type.hostler", standardClass: { epcis: "object", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["mobile", "operable"] }
1118
- ];
1119
-
1120
- // ../ops-contract/dist/mes-profile.js
1121
- var MES_BIZSTEP = {
1122
- receiving: "urn:epcglobal:cbv:bizstep:receiving",
1123
- // 원자재 수령
1124
- producing: "urn:epcglobal:cbv:bizstep:commissioning",
1125
- // 생산(제품 최초 생성)
1126
- storing: "urn:epcglobal:cbv:bizstep:storing"
1127
- // 완제품 저장
1128
- };
1129
- var BTT_PRODORDER = "urn:epcglobal:cbv:btt:prodorder";
1130
- function sgtinUri(companyPrefix, itemRef, serial) {
1131
- return `urn:epc:id:sgtin:${companyPrefix}.${itemRef}.${serial}`;
1132
- }
1133
- var MES_LOCATION_TYPES = ["raw-store", "cut-station", "weld-station", "paint-booth", "assembly-line", "fg-store"];
1134
- var MES_LOCATION_CLS = {
1135
- "raw-store": { epcis: "bizLocation" },
1136
- "cut-station": { isa95: "WorkCenter", epcis: "bizLocation" },
1137
- "weld-station": { isa95: "WorkCenter", epcis: "bizLocation" },
1138
- "paint-booth": { isa95: "WorkCenter", epcis: "bizLocation" },
1139
- "assembly-line": { isa95: "WorkCenter", epcis: "bizLocation" },
1140
- "fg-store": { epcis: "bizLocation" }
1141
- };
1142
- var MES_LOCATION_LEVEL = {
1143
- "raw-store": "StorageZone",
1144
- "cut-station": "WorkCell",
1145
- "weld-station": "WorkCell",
1146
- "paint-booth": "WorkCell",
1147
- "assembly-line": "ProductionLine",
1148
- "fg-store": "StorageZone"
1149
- };
1150
- var MES_TYPES = [
1151
- ...MES_LOCATION_TYPES.map((k) => ({ key: k, role: "location", label: `twin.type.${k}`, standardClass: MES_LOCATION_CLS[k] ?? {}, identity: { scheme: "gs1:SGLN" }, level: MES_LOCATION_LEVEL[k], capabilities: ["storable"] })),
1152
- { key: "cutter", role: "equipment", label: "twin.type.cutter", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] },
1153
- { key: "welder", role: "equipment", label: "twin.type.welder", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] },
1154
- { key: "painter", role: "equipment", label: "twin.type.painter", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] },
1155
- { key: "assembler", role: "equipment", label: "twin.type.assembler", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] }
1156
- ];
1157
-
1158
- // ../ops-contract/dist/ems-profile.js
1159
- var EMS_LOCATION_TYPES = ["incoming", "feeder", "submeter-zone"];
1160
- var METER_DIRECTION = ["import", "export", "bidirectional"];
1161
- var EMS_PROPERTY = {
1162
- /** 계약전력(kW) — 수전·분기 자리에 선언한다. 없으면 계약 대비 판정을 하지 않는다. */
1163
- contractKW: "contract.kW",
1164
- /*
1165
- * ── 계량 지점의 방향 (2026-08-25) ──────────────────────────────────────────
1166
- * **요금은 계통 접속점의 순 유입에 매겨진다.** 그러니 그 지점의 계량기가 무엇을 재는지 모르면
1167
- * 요금을 사실대로 말할 수 없다 — 내보낸 양을 쓴 양으로 세면 계약 대비 판단이 반대로 뒤집힌다.
1114
+ // ../ops-contract/dist/epcis.js
1115
+ var EPCIS_CONTEXT = "https://ref.gs1.org/standards/epcis/2.0.0/epcis-context.jsonld";
1116
+ var UTC_OFFSET = "+00:00";
1117
+ var DISP = {
1118
+ in_progress: "urn:epcglobal:cbv:disp:in_progress",
1119
+ sellable: "urn:epcglobal:cbv:disp:sellable_accessible",
1120
+ reserved: "urn:epcglobal:cbv:disp:reserved",
1121
+ in_transit: "urn:epcglobal:cbv:disp:in_transit",
1122
+ non_sellable: "urn:epcglobal:cbv:disp:non_sellable_other",
1123
+ // 불량/scrap
1124
+ /**
1125
+ * **기한이 지났다** — CBV `expired`.
1168
1126
  *
1169
- * 이 축이 없는 동안 계량 지점의 kW 를 전부 부하로 셌다. 상계 거래를 하는 현장, 축전지를 붙인 현장,
1170
- * 열병합이 있는 현장 — 접속점 계량기가 양쪽을 재는 곳은 늘 있다.
1127
+ * ── 왜 `non_sellable` 로 계산하지 않나 (2026-08-24) ─────────────────────────────
1128
+ * 커널의 `non_sellable` 은 CBV 의 `non_sellable_other`, 즉 **「그 밖의 이유」**다. 기한 지남을 거기
1129
+ * 넣으면 「기한이 지나 못 판다」와 「깨져서 못 판다」가 같은 값이 되고, 화면은 회수·폐기의 사유를
1130
+ * 구별할 수 없다. 식품에서 그 둘은 다른 조치다.
1171
1131
  *
1172
- * **발전기를 계량기로 만드는 것과 다른 일이다.** 발전은 설비의 사실이고(`energyGenerating`), 그것을
1173
- * 계량 지점으로 보내는 것은 여전히 하지 않는다. 이 축은 **접속점 계량기**가 무엇을 재는지 말하는
1174
- * 자리다.
1132
+ * 그리고 표준에 **정확한 낱말이 있다** — 합치는 것은 있는 낱말을 버리는 것이다.
1175
1133
  *
1176
- * 방향은 **관측이 아니라 선언**이다. 표본마다 바뀌는 값이 아니고, 그 계량기가 무엇을 재도록
1177
- * 설치되었는지는 현장이 안다. 그래서 표본(`EnergyRecord`)에 넣지 않고 자원 속성으로 둔다.
1134
+ * ── 기한 날짜와 다른 축이다 ────────────────────────────────────────────────
1135
+ * `ItemState.expiry` 는 **날짜**이고 이것은 **상태**다. 날짜가 있으면 「지났나」는 파생이지만, 원본이
1136
+ * 「기한 지남」을 상태로 선언하는 시스템이 있다 — 그때 이 값은 관측이다.
1178
1137
  *
1179
- * 값은 계량기 자신의 낱말을 쓴다(적산 레지스터가 그렇게 갈려 있다). §`METER_DIRECTION`.
1180
- */
1181
- meterDirection: "meter.direction",
1182
- /*
1183
- * ── 부하 계수 — 시뮬레이션이 전기를 만들 수 있게 (2026-08-14) ───────────────
1184
- * 「이 설비가 돌면 몇 kW 인가」는 **현장이 아는 값**이다. 그래서 타입 기본값을 두지 않는다:
1185
- * 선언하지 않은 설비는 부하를 만들지 않는다. 기본값을 두면 아무도 그 수가 짐작인 줄 모른 채
1186
- * 요금 판정이 그 위에 선다.
1187
- */
1188
- /** 가동 중 소비(kW). */
1189
- ratedKW: "power.ratedKW",
1190
- /** 멈춰 있을 때의 소비(kW). 없으면 멈춘 동안을 **비운다** — 0 이라고 주장하지 않는다. */
1191
- standbyKW: "power.standbyKW",
1192
- /*
1193
- * ── 요금 단가 — 수를 금액으로 바꾸는 선언 (2026-08-17) ─────────────────────
1138
+ * 둘이 어긋나면(날짜는 남았는데 상태가 지남, 또는 그 반대) **어느 쪽이 맞다고 정하지 않는다** —
1139
+ * 아직 그 판정을 세울 근거가 없다. 어긋남의 구분을 없애지 않는 것이 지금의 규율이다.
1194
1140
  *
1195
- * 그동안 트윈은 「최대수요 328kW」·「전력량 1,547kWh」까지 말하고 멈췄다. 그런데 피크를 깎는 일이
1196
- * 돈이 되는 이유는 **요금이 둘로 나뉘기** 때문이다: 사용량(kWh)에 붙는 요금과 **최대수요(kW)에 붙는
1197
- * 기본요금**. 단가가 없으면 그 절반을 말할 수 없고, 그래서 「피크를 깎아 얼마를 아끼나」에 답하지 못했다.
1141
+ * ── 원문으로 확인했다 (2026-08-24) ────────────────────────────────────────
1142
+ * 1차 출처: **CBV Standard Release 2.0, Ratified Jun 2022** §7.2.3 처분 값 표(38개). 이 객체의
1143
+ * 다른 값들(`in_progress`·`sellable_accessible`·`reserved`·`in_transit`·`non_sellable_other`)도
1144
+ * 그 표에 있다.
1198
1145
  *
1199
- * 단가는 **현장의 계약**이다(같은 나라 안에서도 사업자·요금제마다 다르다). 그래서 기본값을 두지
1200
- * 않는다 — 선언하지 않으면 금액을 계산하지 않는다. 짐작한 단가로 낸 금액은 숫자가 있다는 것만으로
1201
- * 사람을 결정으로 밀어붙인다.
1146
+ * **`non_sellable_expired` 를 쓰지 않는 이유**: 그 값은 CBV 1.0 의 것이고 표준이 **폐기**했다 —
1147
+ * 「deprecated in favour of new disposition values expired, damaged, disposed, … introduced in
1148
+ * CBV 1.1」. 폐기된 값을 쓰면 새 소비처가 읽지 못한다.
1202
1149
  *
1203
- * 계약전력(`contract.kW`)과 같은 자리(수전)에 선언한다.
1150
+ * 참고: GS1 어휘 등록처(`ref.gs1.org/cbv/…`)로는 확인할 수 없었다 — **없는 값에도 같은 응답**을
1151
+ * 준다(지어낸 값의 JSON-LD 가 실재 값과 바이트까지 같았다). 그 경로를 근거로 삼지 말 것.
1204
1152
  */
1205
- /** 기본요금 단가 — 최대수요 1kW 당(청구 주기 기준). */
1206
- demandChargePerKW: "tariff.demandChargePerKW",
1153
+ expired: "urn:epcglobal:cbv:disp:expired",
1207
1154
  /**
1208
- * **요금적용전력**(kW) — 기본요금이 실제로 매겨지는 기준. 계약전력과 다른 값이다.
1209
- *
1210
- * 기본요금이 걸리는 수는 이 주기에 잰 최대가 아닌 경우가 많다 — 지난 몇 달의 최고를 끌고 가거나,
1211
- * 약정 용량으로 매기거나, 사업자가 따로 정한다. 그 규칙은 나라와 계약마다 다르므로 커널이 계산하지
1212
- * 않고 **계산된 결과를 받는다.** 이 값이 있으면 기본요금이 조건부 파생이 아니라 사실이 된다.
1213
- *
1214
- * 계약전력(`contract.kW`)을 이 자리에 넣지 말 것. 넘었을 때의 뜻이 다르다 — 계약 초과는 약정
1215
- * 위반이고, 이쪽은 다음 주기의 기본요금이 오른다는 뜻이다.
1216
- */
1217
- billingDemandKW: "tariff.billingDemandKW",
1218
- /** 사용량 단가 — 1kWh 당. */
1219
- energyChargePerKWh: "tariff.energyChargePerKWh",
1220
- /** 통화 — ISO 4217 코드(USD·KRW…). 없으면 금액에 단위를 붙이지 않는다. */
1221
- currency: "tariff.currency",
1222
- /*
1223
- * ── 축전지의 용량과 방전 정책 (2026-08-17) ─────────────────────────────────
1224
- *
1225
- * 「배터리로 피크를 깎는다」를 시뮬이 보이려면 방전을 만들어야 하고, 그것은 **선언에서** 나와야 한다.
1226
- * 임계·최대율·예비를 우리가 정하면 그 수가 어디서 왔는지 아무도 설명할 수 없다.
1227
- *
1228
- * ── 선언한다는 것은 「그 자동화가 있다」는 주장이다 ─────────────────────────
1229
- * 이 정책을 모델에 적으면 **라이브가 아닌 모든 구동이 그 규칙으로 돈다.** 현장에 BESS 제어기가 없는데
1230
- * 적으면 파생 부하가 실제보다 낙관적으로 나온다(피크가 깎인 것으로 보인다). 그러니 「도입하면?」을
1231
- * 묻는 것이라면 선언이 아니라 **what-if 가 덮어쓸 일**이다.
1232
- *
1233
- * 용량이 없으면 방전하지 않는다 — 용량을 모르면 「얼마나 버티나」를 답할 수 없고, 버티는 시간을
1234
- * 모른 채 깎으면 무한히 깎을 수 있다고 주장하는 셈이다.
1155
+ * **폐기됐다 — 남은 것이 없다.** CBV `destroyed` (CBV ontology, `Disp-destroyed`), with the step
1156
+ * `CBV_BIZSTEP.destroying`. Ruled 2026-09-24, shape from plant: the disposition belongs to the quantity
1157
+ * **observed**, which is what is left. A partial scrap leaves the rest `in_progress`; only the last of it —
1158
+ * nothing left — is `destroyed`. Putting it on a remainder would say sound goods were thrown away.
1235
1159
  */
1236
- /** 축전지 용량(kWh) — SOC 를 에너지로 바꾸는 값. 없으면 방전을 만들지 않는다. */
1237
- capacityKWh: "storage.capacityKWh",
1238
- /** 이 순수요를 넘으면 방전한다(kW) — 피크 억제 임계. */
1239
- dischargeAboveKW: "dispatch.dischargeAboveKW",
1240
- /** 최대 방전율(kW) — 인버터가 낼 수 있는 한계. */
1241
- maxDischargeKW: "dispatch.maxDischargeKW",
1242
- /** 예비 SOC(%) — 이 아래로는 쓰지 않는다(비상 대비). 없으면 0 으로 본다. */
1243
- reserveSoc: "dispatch.reserveSoc",
1160
+ destroyed: "urn:epcglobal:cbv:disp:destroyed",
1244
1161
  /**
1245
- * 시뮬레이션이 **출발할 때의 SOC(%)** — 씨앗값이다.
1162
+ * **검사에 합격했다 / 불합격했다** — CBV `conformant` / `non_conformant`.
1246
1163
  *
1247
- * 계측이 SOC 를 알려 주는 트윈에는 필요 없다(잰 값이 진실이다). 시뮬 트윈에는 알려 줄 것이 없어서
1248
- * 「SOC 를 모르면 방전하지 않는다」는 규율에 걸려 **배터리가 아무 일도 하지 못했다** — 정책을 선언해도
1249
- * 피크가 한 톨도 깎이지 않았다(실화면에서 그렇게 났다).
1164
+ * 1차 출처(CBV 2.0 §7.2.3) 정의 그대로다.
1250
1165
  *
1251
- * 이것은 상태의 씨앗이지 계측이 아니다. 그래서 계측이 들어오는 순간 그것이 이긴다.
1252
- */
1253
- initialSoc: "storage.initialSoc",
1254
- /*
1255
- * ── 발전 — 태양광이 서 있기만 하던 자리 (2026-08-18) ────────────────────────
1166
+ * conformant Outcome of a successful/passed inspection in an inspecting or repairing step
1167
+ * non_conformant Outcome of an unsuccessful/failed inspection in an inspecting or repairing step
1256
1168
  *
1257
- * 계통도에 태양광을 그려 놓고 시뮬에서는 한 톨도 만들지 못했다. 미러는 원천이 발전량을 보내 주지만,
1258
- * 시뮬 트윈에는 그것을 만들 근거가 없었다 — 그래서 「태양광을 늘리면 피크가 얼마나 내려가나」를 물을 수
1259
- * 없었다(what-if 의 값이 절반만 성립했다).
1169
+ * ── 왜 시험 결과 축을 자원에 더하지 않고 이것을 쓰나 (2026-08-24) ────────────
1170
+ * 로트의 검사 판정을 담을 자리를 찾다가 `ItemState.testResults` 를 더하려 했다. 그런데 표준은 그
1171
+ * 사실을 **이미 처분으로 말한다**: `bizStep: inspecting` 사건에 이 처분이 붙는다.
1260
1172
  *
1261
- * **형상을 우리가 지어내지 않는다.** 위도·계절·날씨로 곡선을 만들면 그 수가 어디서 왔는지 아무도
1262
- * 설명할 수 없고, 흐린 날 현장의 실적과 어긋난다. 그래서 현장이 **하루 형상을 적는다**: 24개 비율
1263
- * (0~1)이면 그것이 그 현장의 곡선이다(측정한 형상을 그대로 붙일 수 있다).
1173
+ * 처분을 쓰면 두 가지가 공짜로 성립한다.
1174
+ * ① **상태 ⊆ 이벤트** — 처분은 이미 사건에서 온다. 상태에만 있는 축을 만들지 않는다
1175
+ * ② **운영에 곧 닿는다** — 「이 자재를 쓸 수 있나」가 처분으로 답해진다(판정을 따로 읽지 않는다)
1264
1176
  *
1265
- * 정격만 있고 형상이 없으면 발전하지 않는다 — 하루 종일 정격으로 발전하는 태양광은 없다.
1177
+ * 시험의 **자세한 내용**(어느 명세로, 무엇을 재어)은 다른 물음이고, 표준은 그것을 `TestResult` 로
1178
+ * 두며 결과가 대상을 가리킨다(`TestableObjectID`) — 대상이 결과를 들지 않는다. 그 축이 필요해지면
1179
+ * 그때 열되, **판정 자체는 여기서 끝난다.**
1266
1180
  */
1181
+ conformant: "urn:epcglobal:cbv:disp:conformant",
1182
+ non_conformant: "urn:epcglobal:cbv:disp:non_conformant"
1183
+ };
1184
+ var HATIOLAB_VOCABULARY = "https://hatiolab.com/voc/";
1185
+ var PRIVATE_BIZSTEP = {
1186
+ /** 공정에 자재가 들어갔다 — ISA-95 `MaterialUse: Consumed`. */
1187
+ consuming: `${HATIOLAB_VOCABULARY}bizstep/consuming`
1188
+ };
1189
+ var CBV_BIZSTEP = {
1190
+ /* 소비(`consuming`)는 CBV 에 없어 `PRIVATE_BIZSTEP` 으로 갔다(2026-09-24). */
1267
1191
  /**
1268
- * 발전 정격(kW) — **교류 쪽 최대 출력**. 계통으로 나가는 값이다.
1269
- *
1270
- * 이용률의 분모가 이 값이다(만든 양 ÷ 정격 × 시간). **직류 쪽 정격(§`genRatedKWdc`)과 다르고 더 작다** —
1271
- * 두 값을 한 이름에 담으면 이용률이 그 차이만큼 틀리고, 틀린 이유가 아무 데도 표시되지 않는다.
1272
- *
1273
- * 설비마다 선언하거나, 설비별 값을 모르는 현장은 **자리(수전)에 합계로** 선언한다.
1192
+ * **폐기 — 물건이 끝났다.** CBV `destroying`: "Process of terminating an object. For an instance-level
1193
+ * identifier, the object should not be the subject of subsequent events" (CBV ontology, `BizStep-destroying`).
1194
+ * Ruled 2026-09-24 (chief architect) — plant wrote scrapping as `other`; the warehouse's disposal door uses the
1195
+ * same step. Not a count, and not a correction: the goods are gone.
1274
1196
  */
1275
- genRatedKW: "generation.ratedKW",
1197
+ destroying: "urn:epcglobal:cbv:bizstep:destroying",
1198
+ /** 새 물품이 생겨 계보가 시작된다 — ISA-95 `MaterialUse: Produced`. */
1199
+ commissioning: "urn:epcglobal:cbv:bizstep:commissioning",
1276
1200
  /**
1277
- * 발전 정격(kW) — **직류 쪽**. 변환기(인버터)의 입력 쪽 정격이고, 태양광이면 패널 정격의 합이다.
1201
+ * **검사** — CBV `inspecting`. 1차 출처(CBV 2.0) 정의: 「Process of reviewing objects to address
1202
+ * potential physical or documentation defects」이고, 「표본과 달리 검사된 대상은 그대로 남는다」고
1203
+ * 이어진다(즉 검사는 물건을 소비하지 않는다).
1278
1204
  *
1279
- * 이용률의 분모로 쓰지 않는다. 이 값이 교류 정격보다 큰 것은 설계이고(변환기를 그렇게 고른다),
1280
- * 그 차이가 「맑은 정오에도 교류 출력이 더 오르지 않는 이유」를 설명한다.
1205
+ * 이 단계에 `DISP.conformant`/`DISP.non_conformant` 가 붙어 판정이 처분으로 남는다 — 입고검수·
1206
+ * 공정 중 검사가 그 모양이다.
1281
1207
  */
1282
- genRatedKWdc: "generation.ratedKWdc",
1208
+ inspecting: "urn:epcglobal:cbv:bizstep:inspecting",
1283
1209
  /**
1284
- * 하루 형상 — 쉼표로 나눈 **24개 비율**(0~1). 시각(현장 시간대)의 정격 대비 출력이다.
1210
+ * **어느 단계로도 이름 붙지 않는 활동** — 원문: 「A business step not identified by any other」.
1285
1211
  *
1286
- * 예: `0,0,0,0,0,0,0.05,0.2,0.45,0.7,0.9,1,1,0.95,0.8,0.6,0.35,0.12,0.02,0,0,0,0,0`
1212
+ * 이것은 근사가 아니라 **표준이 준 낱말**이다. 그 구별이 중요하다: 재고 조정처럼 한 낱말이 두 일을
1213
+ * 하는 원천(세어 보고 맞춘 것 · 사람이 정정한 것)을 `cycle_counting` 으로 옮기면 **일어나지 않은
1214
+ * 계수를 기록**하게 된다. 그때 쓰는 것이 이 값이고, 무슨 일이었는지는 값으로 함께 나른다.
1215
+ *
1216
+ * 원문 확인: CBV Standard Release 2.0(Ratified Jun 2022) §7.1.
1287
1217
  */
1288
- genDailyProfile: "generation.dailyProfile"
1218
+ other: "urn:epcglobal:cbv:bizstep:other"
1289
1219
  };
1290
- var EMS_PROPERTY_SPEC = {
1291
- [EMS_PROPERTY.contractKW]: { uom: "kW", dataType: "xs:double", note: "contracted power at the metering point, in kW." },
1292
- /* 값이 셋 중 하나여야 한다 — 그 밖의 낱말은 받지 않는다(뜻이 통할 것 같은 말도 받지 않는다). */
1293
- [EMS_PROPERTY.meterDirection]: {
1294
- dataType: "xs:string",
1295
- note: 'what this metering point measures: "import" (drawn from the grid), "export" (generation sent out), or "bidirectional" (one meter for both, sign carries the direction). Left undeclared, the point counts as load \u2014 leaving it out never makes demand look smaller than it is.'
1296
- },
1297
- [EMS_PROPERTY.ratedKW]: { uom: "kW", dataType: "xs:double", note: "power drawn while running, in kW." },
1298
- [EMS_PROPERTY.standbyKW]: { uom: "kW", dataType: "xs:double", note: "power drawn while idle, in kW." },
1299
- [EMS_PROPERTY.demandChargePerKW]: { dataType: "xs:double", note: "demand charge per kW of billing-period peak, in the declared currency." },
1300
- [EMS_PROPERTY.billingDemandKW]: {
1301
- uom: "kW",
1302
- dataType: "xs:double",
1303
- note: "billing demand in kW \u2014 the basis the demand charge is actually billed on (often a rolling 12-month peak). Not the contracted power."
1304
- },
1305
- [EMS_PROPERTY.energyChargePerKWh]: { dataType: "xs:double", note: "energy charge per kWh, in the declared currency." },
1306
- [EMS_PROPERTY.currency]: { dataType: "xs:string", note: "ISO 4217 currency code, e.g. USD or KRW." },
1307
- [EMS_PROPERTY.capacityKWh]: { uom: "kWh", dataType: "xs:double", note: "usable energy of the storage, in kWh." },
1308
- [EMS_PROPERTY.dischargeAboveKW]: { uom: "kW", dataType: "xs:double", note: "net demand above which the storage discharges, in kW." },
1309
- [EMS_PROPERTY.maxDischargeKW]: { uom: "kW", dataType: "xs:double", note: "inverter limit on discharge rate, in kW." },
1310
- /* 퍼센트다 — 0.8 은 0.8% 이고 80% 가 아니다. 이 한 줄이 없어서 배터리가 조용히 비어 있었다. */
1311
- [EMS_PROPERTY.reserveSoc]: { uom: "%", dataType: "xs:double", range: [0, 100], note: "reserve state of charge as a percentage 0-100 (20 means 20%), never discharged below." },
1312
- [EMS_PROPERTY.initialSoc]: { uom: "%", dataType: "xs:double", range: [0, 100], note: "starting state of charge as a percentage 0-100 (80 means 80%, not 0.8)." },
1313
- [EMS_PROPERTY.genRatedKW]: {
1314
- uom: "kW",
1315
- dataType: "xs:double",
1316
- note: "rated AC generation output, in kW \u2014 the denominator of capacity factor. Declare per equipment, or on the incoming location as a site total."
1317
- },
1318
- [EMS_PROPERTY.genRatedKWdc]: {
1319
- uom: "kW",
1320
- dataType: "xs:double",
1321
- note: "rated DC generation capacity, in kW (panel rating for PV). Larger than the AC rating by design; not the capacity-factor denominator."
1322
- },
1323
- [EMS_PROPERTY.genDailyProfile]: {
1324
- dataType: "xs:string",
1325
- note: "daily shape as 24 comma-separated fractions of rated output (0-1), one per hour of local time \u2014 the site declares its own curve; we do not invent one."
1326
- }
1220
+ function sgtinClass(companyPrefix, itemRef) {
1221
+ return `urn:epc:idpat:sgtin:${companyPrefix}.${itemRef}.*`;
1222
+ }
1223
+ var ILMD_ATTR = {
1224
+ /** 유통기한·만료(로트 단위). */
1225
+ expiry: "cbvmda:itemExpirationDate",
1226
+ /** 로트·배치 번호(직렬 개체에 로트를 붙일 때). */
1227
+ lot: "cbvmda:lotNumber"
1327
1228
  };
1328
- function generationFractionAt(profile, hourOfDay) {
1329
- const parts = String(profile ?? "").split(",").map((v2) => v2.trim()).filter((v2) => v2 !== "");
1330
- if (parts.length !== 24)
1331
- return void 0;
1332
- const h = Math.floor(hourOfDay);
1333
- if (!Number.isFinite(h) || h < 0 || h > 23)
1229
+ function lgtinClass(companyPrefix, itemRefAndIndicator, lot) {
1230
+ return `urn:epc:class:lgtin:${companyPrefix}.${itemRefAndIndicator}.${encodeURIComponent(lot)}`;
1231
+ }
1232
+ function parseEpc(uri) {
1233
+ const raw = String(uri ?? "");
1234
+ if (/(?:\/obj\/|:obj:)[^/:\s]+$/.test(raw))
1235
+ return { scheme: "unknown", instance: true, uri: raw };
1236
+ const inHouseClass = raw.match(/(?:\/class\/|:class:)([^/:\s]+)$/);
1237
+ if (inHouseClass) {
1238
+ const at = inHouseClass[1].indexOf(LOT_SEPARATOR);
1239
+ const split = at > 0 ? decodedPair(inHouseClass[1].slice(0, at), inHouseClass[1].slice(at + LOT_SEPARATOR.length)) : void 0;
1240
+ return split ? { scheme: "unknown", instance: false, item: split[0], lot: split[1], uri: raw } : { scheme: "unknown", instance: false, uri: raw };
1241
+ }
1242
+ const cls = raw.match(/^urn:epc:class:lgtin:(.+)$/);
1243
+ if (cls) {
1244
+ const seg = cls[1].split(".");
1245
+ const lot = seg.slice(2).join(".");
1246
+ return {
1247
+ scheme: "lgtin",
1248
+ instance: false,
1249
+ gtinKey: seg.slice(0, 2).join("."),
1250
+ lot: lot ? decodeURIComponent(lot) : void 0,
1251
+ uri: raw
1252
+ };
1253
+ }
1254
+ const pat = raw.match(/^urn:epc:idpat:sgtin:(.+)$/);
1255
+ if (pat) {
1256
+ const seg = pat[1].split(".");
1257
+ return { scheme: "idpat", instance: false, gtinKey: seg.slice(0, 2).join("."), uri: raw };
1258
+ }
1259
+ const id = raw.match(/^urn:epc:id:([a-z]+):(.+)$/);
1260
+ if (id) {
1261
+ const scheme = id[1];
1262
+ const seg = id[2].split(".");
1263
+ const known = ["sgtin", "sscc", "gdti", "grai", "giai", "sgln"].includes(scheme);
1264
+ return {
1265
+ scheme: known ? scheme : "unknown",
1266
+ instance: true,
1267
+ ...scheme === "sgtin" ? { gtinKey: seg.slice(0, 2).join("."), serial: seg[2] } : { serial: seg.slice(1).join(".") },
1268
+ uri: raw
1269
+ };
1270
+ }
1271
+ return { scheme: "unknown", instance: false, uri: raw };
1272
+ }
1273
+ function decodedPair(item, lot) {
1274
+ if (!lot)
1334
1275
  return void 0;
1335
- const v = Number(parts[h]);
1336
- if (!Number.isFinite(v) || v < 0 || v > 1)
1276
+ try {
1277
+ return [decodeURIComponent(item), decodeURIComponent(lot)];
1278
+ } catch {
1337
1279
  return void 0;
1338
- return v;
1280
+ }
1339
1281
  }
1340
- var EMS_TYPES = [
1341
- /* ── 자리: 전기적 구간 ─────────────────────────────────────────────────── */
1342
- {
1343
- key: "incoming",
1344
- role: "location",
1345
- /* 전기 계통의 구간은 **설비 계층의 단이 아니다** — 표준의 탈출구(`Other`)를 쓴다(아래 주석). */
1346
- level: "Other",
1347
- label: "twin.type.incoming",
1348
- /* 수전 지점 — 계약전력이 걸리는 자리이고, 요금의 근거가 되는 수요는 여기서 잰다.
1349
- ISO 50001 의 「에너지 유입」 경계이기도 하다(조직의 에너지 검토가 여기서 시작한다). */
1350
- standardClass: { iec61850: "MMTR", iso50001: "EnergyInput" },
1351
- identity: { scheme: "kernel:id" },
1352
- capabilities: ["metered"]
1353
- },
1354
- {
1355
- key: "feeder",
1356
- role: "location",
1357
- /* 전기 계통의 구간은 **설비 계층의 단이 아니다** — 표준의 탈출구(`Other`)를 쓴다(아래 주석). */
1358
- level: "Other",
1359
- label: "twin.type.feeder",
1360
- /* 분기 회로 — 부하 분해의 단위. 계약전력의 하위 배분이 여기서 정해진다. */
1361
- standardClass: { iec61850: "Feeder", iso50001: "EnergyUse" },
1362
- identity: { scheme: "kernel:id" },
1363
- capabilities: ["metered"]
1364
- },
1365
- {
1366
- key: "submeter-zone",
1367
- role: "location",
1368
- /* 전기 계통의 구간은 **설비 계층의 단이 아니다** — 표준의 탈출구(`Other`)를 쓴다(아래 주석). */
1369
- level: "Other",
1370
- label: "twin.type.submeter-zone",
1371
- /*
1372
- * 구역 계량 — 여러 부하를 한 계량기로 묶어 재는 자리(공조·조명처럼 개별 계량이 없는 것들).
1373
- *
1374
- * ISO 50001 의 **SEU**(유의 에너지 사용처)가 대개 이 알갱이다. 개별 설비까지 재지 못하는 현장이
1375
- * 많고, 그것을 「모른다」로 두는 대신 **묶음으로 아는 것**이 정직하다.
1376
- */
1377
- standardClass: { iec61850: "MMXU", iso50001: "SEU" },
1378
- identity: { scheme: "kernel:id" },
1379
- capabilities: ["metered"]
1380
- },
1381
- /* ── 설비: 계량 지점과 에너지 자원 ──────────────────────────────────────── */
1382
- {
1383
- key: "meter",
1384
- role: "equipment",
1385
- label: "twin.type.meter",
1386
- /* 계량기 — 측정 논리 노드(MMXU=측정단위, MMTR=적산). 자산으로도 하나다(ISO 55000). */
1387
- standardClass: { iec61850: "MMXU", iso55000: "Asset", iso50001: "MeasurementPoint" },
1388
- identity: { scheme: "kernel:id" },
1389
- capabilities: ["metered", "operable"]
1390
- },
1391
- {
1392
- key: "breaker",
1393
- role: "equipment",
1394
- label: "twin.type.breaker",
1395
- /*
1396
- * 차단기 — IEC 61850 `XCBR`. **우리는 이것을 조작하지 않는다**(안전 계통은 범위 밖: ems.md §1).
1397
- * 상태를 읽어 계통 구성을 알 뿐이다.
1398
- *
1399
- * 능력은 `switching` 이다 — `operable` 이 아니다(2026-08-14). `operable` 이던 동안 저작면이 차단기에
1400
- * **제어 컴포넌트**를 권했다: 우리가 하지 않기로 한 조작을 화면이 부추긴 것이다. 「위치를 읽는다」와
1401
- * 「명령을 받는다」는 다른 능력이고, 이제 그 둘이 갈려 있다(capability.ts 주석).
1402
- */
1403
- standardClass: { iec61850: "XCBR", iso55000: "Asset" },
1404
- identity: { scheme: "kernel:id" },
1405
- capabilities: ["switching"]
1406
- },
1407
- {
1408
- key: "pv-array",
1409
- role: "equipment",
1410
- label: "twin.type.pv-array",
1411
- /* 태양광 어레이 — IEC 61850-7-420(분산자원)의 `DPVA`. 발전과 계량은 다른 능력이다. */
1412
- standardClass: { iec61850: "DPVA", iso55000: "Asset", iso50001: "RenewableSupply" },
1413
- identity: { scheme: "kernel:id" },
1414
- capabilities: ["energyGenerating", "metered", "operable"]
1415
- },
1416
- {
1417
- key: "battery",
1418
- role: "equipment",
1419
- label: "twin.type.battery",
1420
- /* 축전지(ESS) — `ZBAT`. 충전·방전을 나눠 재고, 저장은 보관(`storable`)이 아니다(물건이 아니다). */
1421
- standardClass: { iec61850: "ZBAT", iso55000: "Asset" },
1422
- identity: { scheme: "kernel:id" },
1423
- capabilities: ["energyStoring", "metered", "operable"]
1424
- },
1425
- {
1426
- key: "utility",
1427
- role: "equipment",
1428
- label: "twin.type.utility",
1429
- /*
1430
- * 공통 설비 — 공조·컴프레서·칠러·조명·폐수처리처럼 **어느 공정에도 귀속되지 않는** 소비처.
1431
- *
1432
- * ── 왜 따로 있나 (2026-08-14) ────────────────────────────────────────────
1433
- * 물류·생산 트윈에서 이런 것들은 **설비가 아니다**(공정에 매핑되지 않으므로 자원 축에 없다).
1434
- * 그런데 에너지에서는 소비의 절반을 차지하고 감축 후보 1순위다 — 담을 자리가 반드시 있어야 한다.
1435
- *
1436
- * 처음에는 `curtailable-load` 하나로 받으려 했다. 그런데 그 이름은 **「줄일 수 있다」고 주장**한다:
1437
- * 폐수처리·방폭 환기·서버실 냉방은 공통이지만 줄일 수 없고, 그것을 감축 가능으로 두면 트윈이
1438
- * 「이걸 줄이면 됩니다」라는 거짓 제안을 한다. 공통성과 감축 가능성은 **다른 축**이다.
1439
- *
1440
- * 표준: IEC 61850 에 「공통 설비」라는 논리 노드는 없다(설비 종류마다 다른 노드다) — 비운다.
1441
- * ISO 50001 의 SEU 는 이 부류를 가장 많이 가리킨다(유의 에너지 사용처).
1442
- */
1443
- standardClass: { iso50001: "SEU", iso55000: "Asset" },
1444
- identity: { scheme: "kernel:id" },
1445
- capabilities: ["metered", "operable"]
1446
- },
1447
- {
1448
- key: "curtailable-load",
1449
- role: "equipment",
1450
- label: "twin.type.curtailable-load",
1451
- /*
1452
- * 감축 가능 부하 — 줄일 수 있는 소비처(공조·충전기·비상시 미가동 라인).
1453
- *
1454
- * 표준에 이 이름은 없다: IEC 61850 은 설비를 종류로 부르고 「감축 가능」은 **운영 정책**이다.
1455
- * 그래서 `iec61850` 칸을 비우고 ISO 50001 의 SEU 로만 대응한다 — 억지로 논리 노드를 적으면
1456
- * 적합성 표가 거짓을 말한다.
1457
- */
1458
- standardClass: { iso50001: "SEU" },
1459
- identity: { scheme: "kernel:id" },
1460
- capabilities: ["curtailable", "metered", "operable"]
1282
+ function itemClassOf(uri) {
1283
+ if (!uri)
1284
+ return void 0;
1285
+ const parsed = parseEpc(uri);
1286
+ if (parsed.scheme === "lgtin" && parsed.gtinKey)
1287
+ return `urn:epc:idpat:sgtin:${parsed.gtinKey}.*`;
1288
+ if (parsed.item !== void 0) {
1289
+ const marker = /^(.*(?:\/class\/|:class:))[^/:\s]+$/.exec(parsed.uri);
1290
+ if (marker)
1291
+ return `${marker[1]}${parsed.item.replace(STRUCTURE_BREAKING, (c) => PERCENT_ENCODED[c])}`;
1461
1292
  }
1462
- ];
1463
- function electricalUpstreamOf(locations, id) {
1464
- const byId2 = new Map((locations ?? []).filter((l) => l?.id).map((l) => [String(l.id), l]));
1465
- const self = byId2.get(String(id));
1466
- if (!self)
1293
+ return parsed.uri;
1294
+ }
1295
+ function lotClassOfItem(itemClass, lot) {
1296
+ if (!itemClass)
1467
1297
  return void 0;
1468
- if (self.upstreamId)
1469
- return String(self.upstreamId);
1470
- const parent = self.parentId ? byId2.get(String(self.parentId)) : void 0;
1471
- if (!parent)
1298
+ const idpat = /^urn:epc:idpat:sgtin:(\d+)\.(\d+)\.\*$/.exec(itemClass);
1299
+ if (idpat)
1300
+ return lotClassUri({ namespace: "", item: "", lot, gtin: `${idpat[1]}.${idpat[2]}` });
1301
+ const parsed = parseEpc(itemClass);
1302
+ if (parsed.instance || parsed.lot !== void 0)
1472
1303
  return void 0;
1473
- return isElectricalLocationType(String(parent.type ?? "")) ? String(parent.id) : void 0;
1304
+ const url = /^(https?:\/\/.+)\/class\/([^/:\s]+)$/.exec(itemClass);
1305
+ const urn = /^(urn:.+):class:([^/:\s]+)$/.exec(itemClass);
1306
+ const at = url ?? urn;
1307
+ if (!at)
1308
+ return void 0;
1309
+ let item;
1310
+ try {
1311
+ item = decodeURIComponent(at[2]);
1312
+ } catch {
1313
+ return void 0;
1314
+ }
1315
+ return lotClassUri({ namespace: at[1], item, lot });
1474
1316
  }
1475
- function isElectricalLocationType(type) {
1476
- return EMS_LOCATION_TYPES.includes(type);
1477
- }
1478
-
1479
- // ../ops-contract/dist/domain-catalog.js
1480
- var locationKeys = (types) => types.filter((t) => t.role === "location").map((t) => t.key);
1481
- var DOMAIN_CATALOG = {
1482
- // label 은 언어 중립 i18n 키(twin.system.<code>) — 사람 언어는 표현계층이 렌더(L2).
1483
- wms: { system: "wms", label: "twin.system.wms", types: WMS_TYPES, locationTypes: locationKeys(WMS_TYPES) },
1484
- yms: { system: "yms", label: "twin.system.yms", types: YMS_TYPES, locationTypes: locationKeys(YMS_TYPES) },
1485
- mes: { system: "mes", label: "twin.system.mes", types: MES_TYPES, locationTypes: locationKeys(MES_TYPES) },
1486
- ems: { system: "ems", label: "twin.system.ems", types: EMS_TYPES, locationTypes: locationKeys(EMS_TYPES) }
1487
- };
1488
-
1489
- // ../ops-contract/dist/domain-definition.js
1490
- var OP_PARAM = {
1491
- /** 양품률(0..1, 무차원). 없으면 커널 기본값 — 기본값을 쓴 사실은 `specCoverage()` 가 밝힌다. */
1492
- yield: "yield",
1493
- /** 셋업·체인지오버 소요(ISO 8601 기간 문자열). ISA-95 는 셋업을 별도 세그먼트로도 표현하지만,
1494
- * 현재 커널은 작업에 붙는 셋업으로 다루므로 모수로 받는다. */
1495
- setupDuration: "setupDuration"
1496
- };
1497
- function procedureViolations(route) {
1498
- const elements = route.procedure ?? [];
1499
- if (!elements.length)
1500
- return [];
1501
- const errors = [];
1502
- const byKey = new Map(elements.map((e) => [e.key, e]));
1503
- const hasChild = new Set(elements.map((e) => e.parent).filter((k) => !!k));
1504
- const stepSet = new Set(route.steps ?? []);
1505
- const seenStep = /* @__PURE__ */ new Map();
1506
- for (const n of elements) {
1507
- if (n.parent && !byKey.has(n.parent))
1508
- errors.push(`${n.key}: \uC0C1\uC704 '${n.parent}' \uAC00 \uC774 \uC808\uCC28\uC5D0 \uC5C6\uB2E4`);
1509
- if (n.step === void 0)
1510
- continue;
1511
- if (hasChild.has(n.key))
1512
- errors.push(`${n.key}: \uC544\uB798 \uC694\uC18C\uAC00 \uC788\uB294\uB370 \uB2E8\uACC4\uB97C \uB4E0\uB2E4 \u2014 \uC2E4\uD589\uB418\uB294 \uAC83\uC740 \uC78E\uBFD0\uC774\uB2E4`);
1513
- if (!stepSet.has(n.step))
1514
- errors.push(`${n.key}: \uBAA8\uB974\uB294 \uB2E8\uACC4 '${n.step}' \u2014 steps \uC5D0 \uC5C6\uB2E4`);
1515
- const already = seenStep.get(n.step);
1516
- if (already)
1517
- errors.push(`\uB2E8\uACC4 '${n.step}' \uAC00 '${already}' \uC640 '${n.key}' \uB458\uC5D0 \uC2E4\uB838\uB2E4 \u2014 \uC808\uCC28 \uC5B4\uB514\uC778\uC9C0 \uB9D0\uD560 \uC218 \uC5C6\uB2E4`);
1518
- else
1519
- seenStep.set(n.step, n.key);
1520
- }
1521
- return errors;
1522
- }
1523
-
1524
- // ../ops-contract/dist/energy-ingest.js
1525
- function resolveSubject(kind, localId, opts) {
1526
- const declared = opts.identityOf?.(kind, localId);
1527
- if (declared)
1528
- return { subject: declared, basis: "declared" };
1529
- return { subject: opts.scopeId ? `${opts.scopeId}/${localId}` : localId, basis: "twin-local" };
1530
- }
1531
- function periodFactId(tenantId, kind, subject, from, to) {
1532
- return [tenantId, kind, subject, from, to].join("|");
1533
- }
1534
-
1535
- // ../ops-contract/dist/epcis.js
1536
- var EPCIS_CONTEXT = "https://ref.gs1.org/standards/epcis/2.0.0/epcis-context.jsonld";
1537
- var UTC_OFFSET = "+00:00";
1538
- var DISP = {
1539
- in_progress: "urn:epcglobal:cbv:disp:in_progress",
1540
- sellable: "urn:epcglobal:cbv:disp:sellable_accessible",
1541
- reserved: "urn:epcglobal:cbv:disp:reserved",
1542
- in_transit: "urn:epcglobal:cbv:disp:in_transit",
1543
- non_sellable: "urn:epcglobal:cbv:disp:non_sellable_other",
1544
- // 불량/scrap
1545
- /**
1546
- * **기한이 지났다** — CBV `expired`.
1547
- *
1548
- * ── 왜 `non_sellable` 로 계산하지 않나 (2026-08-24) ─────────────────────────────
1549
- * 커널의 `non_sellable` 은 CBV 의 `non_sellable_other`, 즉 **「그 밖의 이유」**다. 기한 지남을 거기
1550
- * 넣으면 「기한이 지나 못 판다」와 「깨져서 못 판다」가 같은 값이 되고, 화면은 회수·폐기의 사유를
1551
- * 구별할 수 없다. 식품에서 그 둘은 다른 조치다.
1552
- *
1553
- * 그리고 표준에 **정확한 낱말이 있다** — 합치는 것은 있는 낱말을 버리는 것이다.
1554
- *
1555
- * ── 기한 날짜와 다른 축이다 ────────────────────────────────────────────────
1556
- * `ItemState.expiry` 는 **날짜**이고 이것은 **상태**다. 날짜가 있으면 「지났나」는 파생이지만, 원본이
1557
- * 「기한 지남」을 상태로 선언하는 시스템이 있다 — 그때 이 값은 관측이다.
1558
- *
1559
- * 둘이 어긋나면(날짜는 남았는데 상태가 지남, 또는 그 반대) **어느 쪽이 맞다고 정하지 않는다** —
1560
- * 아직 그 판정을 세울 근거가 없다. 어긋남의 구분을 없애지 않는 것이 지금의 규율이다.
1561
- *
1562
- * ── 원문으로 확인했다 (2026-08-24) ────────────────────────────────────────
1563
- * 1차 출처: **CBV Standard Release 2.0, Ratified Jun 2022** §7.2.3 처분 값 표(38개). 이 객체의
1564
- * 다른 값들(`in_progress`·`sellable_accessible`·`reserved`·`in_transit`·`non_sellable_other`)도
1565
- * 그 표에 있다.
1566
- *
1567
- * **`non_sellable_expired` 를 쓰지 않는 이유**: 그 값은 CBV 1.0 의 것이고 표준이 **폐기**했다 —
1568
- * 「deprecated in favour of new disposition values expired, damaged, disposed, … introduced in
1569
- * CBV 1.1」. 폐기된 값을 쓰면 새 소비처가 읽지 못한다.
1570
- *
1571
- * 참고: GS1 어휘 등록처(`ref.gs1.org/cbv/…`)로는 확인할 수 없었다 — **없는 값에도 같은 응답**을
1572
- * 준다(지어낸 값의 JSON-LD 가 실재 값과 바이트까지 같았다). 그 경로를 근거로 삼지 말 것.
1573
- */
1574
- expired: "urn:epcglobal:cbv:disp:expired",
1575
- /**
1576
- * **검사에 합격했다 / 불합격했다** — CBV `conformant` / `non_conformant`.
1577
- *
1578
- * 1차 출처(CBV 2.0 §7.2.3) 정의 그대로다.
1579
- *
1580
- * conformant Outcome of a successful/passed inspection in an inspecting or repairing step
1581
- * non_conformant Outcome of an unsuccessful/failed inspection in an inspecting or repairing step
1582
- *
1583
- * ── 왜 시험 결과 축을 자원에 더하지 않고 이것을 쓰나 (2026-08-24) ────────────
1584
- * 로트의 검사 판정을 담을 자리를 찾다가 `ItemState.testResults` 를 더하려 했다. 그런데 표준은 그
1585
- * 사실을 **이미 처분으로 말한다**: `bizStep: inspecting` 사건에 이 처분이 붙는다.
1586
- *
1587
- * 처분을 쓰면 두 가지가 공짜로 성립한다.
1588
- * ① **상태 ⊆ 이벤트** — 처분은 이미 사건에서 온다. 상태에만 있는 축을 만들지 않는다
1589
- * ② **운영에 곧 닿는다** — 「이 자재를 쓸 수 있나」가 처분으로 답해진다(판정을 따로 읽지 않는다)
1590
- *
1591
- * 시험의 **자세한 내용**(어느 명세로, 무엇을 재어)은 다른 물음이고, 표준은 그것을 `TestResult` 로
1592
- * 두며 결과가 대상을 가리킨다(`TestableObjectID`) — 대상이 결과를 들지 않는다. 그 축이 필요해지면
1593
- * 그때 열되, **판정 자체는 여기서 끝난다.**
1594
- */
1595
- conformant: "urn:epcglobal:cbv:disp:conformant",
1596
- non_conformant: "urn:epcglobal:cbv:disp:non_conformant"
1597
- };
1598
- var CBV_BIZSTEP = {
1599
- /** 공정에 자재가 들어갔다 — ISA-95 `MaterialUse: Consumed`. */
1600
- consuming: "urn:epcglobal:cbv:bizstep:consuming",
1601
- /** 새 물품이 생겨 계보가 시작된다 — ISA-95 `MaterialUse: Produced`. */
1602
- commissioning: "urn:epcglobal:cbv:bizstep:commissioning",
1603
- /**
1604
- * **검사** — CBV `inspecting`. 1차 출처(CBV 2.0) 정의: 「Process of reviewing objects to address
1605
- * potential physical or documentation defects」이고, 「표본과 달리 검사된 대상은 그대로 남는다」고
1606
- * 이어진다(즉 검사는 물건을 소비하지 않는다).
1607
- *
1608
- * 이 단계에 `DISP.conformant`/`DISP.non_conformant` 가 붙어 판정이 처분으로 남는다 — 입고검수·
1609
- * 공정 중 검사가 그 모양이다.
1610
- */
1611
- inspecting: "urn:epcglobal:cbv:bizstep:inspecting",
1612
- /**
1613
- * **어느 단계로도 이름 붙지 않는 활동** — 원문: 「A business step not identified by any other」.
1614
- *
1615
- * 이것은 근사가 아니라 **표준이 준 낱말**이다. 그 구별이 중요하다: 재고 조정처럼 한 낱말이 두 일을
1616
- * 하는 원천(세어 보고 맞춘 것 · 사람이 정정한 것)을 `cycle_counting` 으로 옮기면 **일어나지 않은
1617
- * 계수를 기록**하게 된다. 그때 쓰는 것이 이 값이고, 무슨 일이었는지는 값으로 함께 나른다.
1618
- *
1619
- * 원문 확인: CBV Standard Release 2.0(Ratified Jun 2022) §7.1.
1620
- */
1621
- other: "urn:epcglobal:cbv:bizstep:other"
1622
- };
1623
- function sgtinClass(companyPrefix, itemRef) {
1624
- return `urn:epc:idpat:sgtin:${companyPrefix}.${itemRef}.*`;
1625
- }
1626
- var ILMD_ATTR = {
1627
- /** 유통기한·만료(로트 단위). */
1628
- expiry: "cbvmda:itemExpirationDate",
1629
- /** 로트·배치 번호(직렬 개체에 로트를 붙일 때). */
1630
- lot: "cbvmda:lotNumber"
1631
- };
1632
- function lgtinClass(companyPrefix, itemRefAndIndicator, lot) {
1633
- return `urn:epc:class:lgtin:${companyPrefix}.${itemRefAndIndicator}.${encodeURIComponent(lot)}`;
1634
- }
1635
- function parseEpc(uri) {
1636
- const raw = String(uri ?? "");
1637
- if (/(?:\/obj\/|:obj:)[^/:\s]+$/.test(raw))
1638
- return { scheme: "unknown", instance: true, uri: raw };
1639
- const inHouseClass = raw.match(/(?:\/class\/|:class:)([^/:\s]+)$/);
1640
- if (inHouseClass) {
1641
- const at = inHouseClass[1].indexOf(LOT_SEPARATOR);
1642
- const split = at > 0 ? decodedPair(inHouseClass[1].slice(0, at), inHouseClass[1].slice(at + LOT_SEPARATOR.length)) : void 0;
1643
- return split ? { scheme: "unknown", instance: false, item: split[0], lot: split[1], uri: raw } : { scheme: "unknown", instance: false, uri: raw };
1644
- }
1645
- const cls = raw.match(/^urn:epc:class:lgtin:(.+)$/);
1646
- if (cls) {
1647
- const seg = cls[1].split(".");
1648
- const lot = seg.slice(2).join(".");
1649
- return {
1650
- scheme: "lgtin",
1651
- instance: false,
1652
- gtinKey: seg.slice(0, 2).join("."),
1653
- lot: lot ? decodeURIComponent(lot) : void 0,
1654
- uri: raw
1655
- };
1656
- }
1657
- const pat = raw.match(/^urn:epc:idpat:sgtin:(.+)$/);
1658
- if (pat) {
1659
- const seg = pat[1].split(".");
1660
- return { scheme: "idpat", instance: false, gtinKey: seg.slice(0, 2).join("."), uri: raw };
1661
- }
1662
- const id = raw.match(/^urn:epc:id:([a-z]+):(.+)$/);
1663
- if (id) {
1664
- const scheme = id[1];
1665
- const seg = id[2].split(".");
1666
- const known = ["sgtin", "sscc", "gdti", "grai", "giai", "sgln"].includes(scheme);
1667
- return {
1668
- scheme: known ? scheme : "unknown",
1669
- instance: true,
1670
- ...scheme === "sgtin" ? { gtinKey: seg.slice(0, 2).join("."), serial: seg[2] } : { serial: seg.slice(1).join(".") },
1671
- uri: raw
1672
- };
1673
- }
1674
- return { scheme: "unknown", instance: false, uri: raw };
1675
- }
1676
- function decodedPair(item, lot) {
1677
- if (!lot)
1678
- return void 0;
1679
- try {
1680
- return [decodeURIComponent(item), decodeURIComponent(lot)];
1681
- } catch {
1682
- return void 0;
1683
- }
1684
- }
1685
- function itemClassOf(uri) {
1686
- if (!uri)
1687
- return void 0;
1688
- const parsed = parseEpc(uri);
1689
- if (parsed.scheme === "lgtin" && parsed.gtinKey)
1690
- return `urn:epc:idpat:sgtin:${parsed.gtinKey}.*`;
1691
- if (parsed.item !== void 0) {
1692
- const marker = /^(.*(?:\/class\/|:class:))[^/:\s]+$/.exec(parsed.uri);
1693
- if (marker)
1694
- return `${marker[1]}${parsed.item.replace(STRUCTURE_BREAKING, (c) => PERCENT_ENCODED[c])}`;
1695
- }
1696
- return parsed.uri;
1697
- }
1698
- function lotClassOfItem(itemClass, lot) {
1699
- if (!itemClass)
1700
- return void 0;
1701
- const idpat = /^urn:epc:idpat:sgtin:(\d+)\.(\d+)\.\*$/.exec(itemClass);
1702
- if (idpat)
1703
- return lotClassUri({ namespace: "", item: "", lot, gtin: `${idpat[1]}.${idpat[2]}` });
1704
- const parsed = parseEpc(itemClass);
1705
- if (parsed.instance || parsed.lot !== void 0)
1706
- return void 0;
1707
- const url = /^(https?:\/\/.+)\/class\/([^/:\s]+)$/.exec(itemClass);
1708
- const urn = /^(urn:.+):class:([^/:\s]+)$/.exec(itemClass);
1709
- const at = url ?? urn;
1710
- if (!at)
1711
- return void 0;
1712
- let item;
1713
- try {
1714
- item = decodeURIComponent(at[2]);
1715
- } catch {
1716
- return void 0;
1717
- }
1718
- return lotClassUri({ namespace: at[1], item, lot });
1719
- }
1720
- function sameItemClass(a, b) {
1721
- const x = itemClassOf(a);
1722
- return x !== void 0 && x === itemClassOf(b);
1317
+ function sameItemClass(a, b) {
1318
+ const x = itemClassOf(a);
1319
+ return x !== void 0 && x === itemClassOf(b);
1723
1320
  }
1724
1321
  function gdtiUri(companyPrefix, docType, serial) {
1725
1322
  return `urn:epc:id:gdti:${companyPrefix}.${docType}.${serial}`;
@@ -1909,6 +1506,448 @@ function gs1KeyDigitViolation(uri) {
1909
1506
  return `${m[1]} \uC790\uB9AC \uC218 \uC624\uB958: ${uri} \u2014 \uD68C\uC0AC \uD504\uB9AC\uD53D\uC2A4(${prefix.length}) + \uCC38\uC870(${ref.length}) = ${got} \uC774\uC9C0\uB9CC ${want} \uC5EC\uC57C \uD55C\uB2E4(GS1 TDS). \uD504\uB9AC\uD53D\uC2A4\uB97C \uBC14\uAFB8\uBA74 \uCC38\uC870 \uC790\uB9AC \uC218\uB97C \uD568\uAED8 \uB9DE\uCDB0\uC57C \uD55C\uB2E4.`;
1910
1507
  }
1911
1508
 
1509
+ // ../ops-contract/dist/yms-profile.js
1510
+ var YARD_BIZSTEP = {
1511
+ arriving: "urn:epcglobal:cbv:bizstep:arriving",
1512
+ // 게이트-인
1513
+ loading: "urn:epcglobal:cbv:bizstep:loading",
1514
+ // 도크 작업(상차: 트레일러←화물)
1515
+ unloading: "urn:epcglobal:cbv:bizstep:unloading",
1516
+ // 도크 작업(하차: 트레일러→화물)
1517
+ departing: "urn:epcglobal:cbv:bizstep:departing"
1518
+ // 게이트-아웃
1519
+ };
1520
+ var YARD_PRIVATE_BIZSTEP = {
1521
+ staging: `${HATIOLAB_VOCABULARY}bizstep/staging`
1522
+ // 야드 슬롯 주차·대기
1523
+ };
1524
+ var YMS_LOCATION_TYPES = ["gate", "yard-slot", "dock-door", "staging"];
1525
+ var YMS_LOCATION_LEVEL = {
1526
+ gate: "Other",
1527
+ "yard-slot": "StorageUnit",
1528
+ "dock-door": "StorageUnit",
1529
+ staging: "StorageZone"
1530
+ };
1531
+ var YMS_TYPES = [
1532
+ ...YMS_LOCATION_TYPES.map((k) => ({ key: k, role: "location", label: `twin.type.${k}`, standardClass: { epcis: "bizLocation" }, identity: { scheme: "gs1:SGLN" }, level: YMS_LOCATION_LEVEL[k], capabilities: ["storable"] })),
1533
+ { key: "hostler", role: "equipment", label: "twin.type.hostler", standardClass: { epcis: "object", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["mobile", "operable"] }
1534
+ ];
1535
+
1536
+ // ../ops-contract/dist/mes-profile.js
1537
+ var MES_BIZSTEP = {
1538
+ receiving: "urn:epcglobal:cbv:bizstep:receiving",
1539
+ // 원자재 수령
1540
+ producing: "urn:epcglobal:cbv:bizstep:commissioning",
1541
+ // 생산(제품 최초 생성)
1542
+ storing: "urn:epcglobal:cbv:bizstep:storing"
1543
+ // 완제품 저장
1544
+ };
1545
+ var BTT_PRODORDER = "urn:epcglobal:cbv:btt:prodorder";
1546
+ function sgtinUri(companyPrefix, itemRef, serial) {
1547
+ return `urn:epc:id:sgtin:${companyPrefix}.${itemRef}.${serial}`;
1548
+ }
1549
+ var MES_LOCATION_TYPES = ["raw-store", "cut-station", "weld-station", "paint-booth", "assembly-line", "fg-store"];
1550
+ var MES_LOCATION_CLS = {
1551
+ "raw-store": { epcis: "bizLocation" },
1552
+ "cut-station": { isa95: "WorkCenter", epcis: "bizLocation" },
1553
+ "weld-station": { isa95: "WorkCenter", epcis: "bizLocation" },
1554
+ "paint-booth": { isa95: "WorkCenter", epcis: "bizLocation" },
1555
+ "assembly-line": { isa95: "WorkCenter", epcis: "bizLocation" },
1556
+ "fg-store": { epcis: "bizLocation" }
1557
+ };
1558
+ var MES_LOCATION_LEVEL = {
1559
+ "raw-store": "StorageZone",
1560
+ "cut-station": "WorkCell",
1561
+ "weld-station": "WorkCell",
1562
+ "paint-booth": "WorkCell",
1563
+ "assembly-line": "ProductionLine",
1564
+ "fg-store": "StorageZone"
1565
+ };
1566
+ var MES_TYPES = [
1567
+ ...MES_LOCATION_TYPES.map((k) => ({ key: k, role: "location", label: `twin.type.${k}`, standardClass: MES_LOCATION_CLS[k] ?? {}, identity: { scheme: "gs1:SGLN" }, level: MES_LOCATION_LEVEL[k], capabilities: ["storable"] })),
1568
+ { key: "cutter", role: "equipment", label: "twin.type.cutter", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] },
1569
+ { key: "welder", role: "equipment", label: "twin.type.welder", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] },
1570
+ { key: "painter", role: "equipment", label: "twin.type.painter", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] },
1571
+ { key: "assembler", role: "equipment", label: "twin.type.assembler", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] }
1572
+ ];
1573
+
1574
+ // ../ops-contract/dist/ems-profile.js
1575
+ var EMS_LOCATION_TYPES = ["incoming", "feeder", "submeter-zone"];
1576
+ var METER_DIRECTION = ["import", "export", "bidirectional"];
1577
+ var EMS_PROPERTY = {
1578
+ /** 계약전력(kW) — 수전·분기 자리에 선언한다. 없으면 계약 대비 판정을 하지 않는다. */
1579
+ contractKW: "contract.kW",
1580
+ /*
1581
+ * ── 계량 지점의 방향 (2026-08-25) ──────────────────────────────────────────
1582
+ * **요금은 계통 접속점의 순 유입에 매겨진다.** 그러니 그 지점의 계량기가 무엇을 재는지 모르면
1583
+ * 요금을 사실대로 말할 수 없다 — 내보낸 양을 쓴 양으로 세면 계약 대비 판단이 반대로 뒤집힌다.
1584
+ *
1585
+ * 이 축이 없는 동안 계량 지점의 kW 를 전부 부하로 셌다. 상계 거래를 하는 현장, 축전지를 붙인 현장,
1586
+ * 열병합이 있는 현장 — 접속점 계량기가 양쪽을 재는 곳은 늘 있다.
1587
+ *
1588
+ * **발전기를 계량기로 만드는 것과 다른 일이다.** 발전은 설비의 사실이고(`energyGenerating`), 그것을
1589
+ * 계량 지점으로 보내는 것은 여전히 하지 않는다. 이 축은 **접속점 계량기**가 무엇을 재는지 말하는
1590
+ * 자리다.
1591
+ *
1592
+ * 방향은 **관측이 아니라 선언**이다. 표본마다 바뀌는 값이 아니고, 그 계량기가 무엇을 재도록
1593
+ * 설치되었는지는 현장이 안다. 그래서 표본(`EnergyRecord`)에 넣지 않고 자원 속성으로 둔다.
1594
+ *
1595
+ * 값은 계량기 자신의 낱말을 쓴다(적산 레지스터가 그렇게 갈려 있다). §`METER_DIRECTION`.
1596
+ */
1597
+ meterDirection: "meter.direction",
1598
+ /*
1599
+ * ── 부하 계수 — 시뮬레이션이 전기를 만들 수 있게 (2026-08-14) ───────────────
1600
+ * 「이 설비가 돌면 몇 kW 인가」는 **현장이 아는 값**이다. 그래서 타입 기본값을 두지 않는다:
1601
+ * 선언하지 않은 설비는 부하를 만들지 않는다. 기본값을 두면 아무도 그 수가 짐작인 줄 모른 채
1602
+ * 요금 판정이 그 위에 선다.
1603
+ */
1604
+ /** 가동 중 소비(kW). */
1605
+ ratedKW: "power.ratedKW",
1606
+ /** 멈춰 있을 때의 소비(kW). 없으면 멈춘 동안을 **비운다** — 0 이라고 주장하지 않는다. */
1607
+ standbyKW: "power.standbyKW",
1608
+ /*
1609
+ * ── 요금 단가 — 수를 금액으로 바꾸는 선언 (2026-08-17) ─────────────────────
1610
+ *
1611
+ * 그동안 트윈은 「최대수요 328kW」·「전력량 1,547kWh」까지 말하고 멈췄다. 그런데 피크를 깎는 일이
1612
+ * 돈이 되는 이유는 **요금이 둘로 나뉘기** 때문이다: 사용량(kWh)에 붙는 요금과 **최대수요(kW)에 붙는
1613
+ * 기본요금**. 단가가 없으면 그 절반을 말할 수 없고, 그래서 「피크를 깎아 얼마를 아끼나」에 답하지 못했다.
1614
+ *
1615
+ * 단가는 **현장의 계약**이다(같은 나라 안에서도 사업자·요금제마다 다르다). 그래서 기본값을 두지
1616
+ * 않는다 — 선언하지 않으면 금액을 계산하지 않는다. 짐작한 단가로 낸 금액은 숫자가 있다는 것만으로
1617
+ * 사람을 결정으로 밀어붙인다.
1618
+ *
1619
+ * 계약전력(`contract.kW`)과 같은 자리(수전)에 선언한다.
1620
+ */
1621
+ /** 기본요금 단가 — 최대수요 1kW 당(청구 주기 기준). */
1622
+ demandChargePerKW: "tariff.demandChargePerKW",
1623
+ /**
1624
+ * **요금적용전력**(kW) — 기본요금이 실제로 매겨지는 기준. 계약전력과 다른 값이다.
1625
+ *
1626
+ * 기본요금이 걸리는 수는 이 주기에 잰 최대가 아닌 경우가 많다 — 지난 몇 달의 최고를 끌고 가거나,
1627
+ * 약정 용량으로 매기거나, 사업자가 따로 정한다. 그 규칙은 나라와 계약마다 다르므로 커널이 계산하지
1628
+ * 않고 **계산된 결과를 받는다.** 이 값이 있으면 기본요금이 조건부 파생이 아니라 사실이 된다.
1629
+ *
1630
+ * 계약전력(`contract.kW`)을 이 자리에 넣지 말 것. 넘었을 때의 뜻이 다르다 — 계약 초과는 약정
1631
+ * 위반이고, 이쪽은 다음 주기의 기본요금이 오른다는 뜻이다.
1632
+ */
1633
+ billingDemandKW: "tariff.billingDemandKW",
1634
+ /** 사용량 단가 — 1kWh 당. */
1635
+ energyChargePerKWh: "tariff.energyChargePerKWh",
1636
+ /** 통화 — ISO 4217 코드(USD·KRW…). 없으면 금액에 단위를 붙이지 않는다. */
1637
+ currency: "tariff.currency",
1638
+ /*
1639
+ * ── 축전지의 용량과 방전 정책 (2026-08-17) ─────────────────────────────────
1640
+ *
1641
+ * 「배터리로 피크를 깎는다」를 시뮬이 보이려면 방전을 만들어야 하고, 그것은 **선언에서** 나와야 한다.
1642
+ * 임계·최대율·예비를 우리가 정하면 그 수가 어디서 왔는지 아무도 설명할 수 없다.
1643
+ *
1644
+ * ── 선언한다는 것은 「그 자동화가 있다」는 주장이다 ─────────────────────────
1645
+ * 이 정책을 모델에 적으면 **라이브가 아닌 모든 구동이 그 규칙으로 돈다.** 현장에 BESS 제어기가 없는데
1646
+ * 적으면 파생 부하가 실제보다 낙관적으로 나온다(피크가 깎인 것으로 보인다). 그러니 「도입하면?」을
1647
+ * 묻는 것이라면 선언이 아니라 **what-if 가 덮어쓸 일**이다.
1648
+ *
1649
+ * 용량이 없으면 방전하지 않는다 — 용량을 모르면 「얼마나 버티나」를 답할 수 없고, 버티는 시간을
1650
+ * 모른 채 깎으면 무한히 깎을 수 있다고 주장하는 셈이다.
1651
+ */
1652
+ /** 축전지 용량(kWh) — SOC 를 에너지로 바꾸는 값. 없으면 방전을 만들지 않는다. */
1653
+ capacityKWh: "storage.capacityKWh",
1654
+ /** 이 순수요를 넘으면 방전한다(kW) — 피크 억제 임계. */
1655
+ dischargeAboveKW: "dispatch.dischargeAboveKW",
1656
+ /** 최대 방전율(kW) — 인버터가 낼 수 있는 한계. */
1657
+ maxDischargeKW: "dispatch.maxDischargeKW",
1658
+ /** 예비 SOC(%) — 이 아래로는 쓰지 않는다(비상 대비). 없으면 0 으로 본다. */
1659
+ reserveSoc: "dispatch.reserveSoc",
1660
+ /**
1661
+ * 시뮬레이션이 **출발할 때의 SOC(%)** — 씨앗값이다.
1662
+ *
1663
+ * 계측이 SOC 를 알려 주는 트윈에는 필요 없다(잰 값이 진실이다). 시뮬 트윈에는 알려 줄 것이 없어서
1664
+ * 「SOC 를 모르면 방전하지 않는다」는 규율에 걸려 **배터리가 아무 일도 하지 못했다** — 정책을 선언해도
1665
+ * 피크가 한 톨도 깎이지 않았다(실화면에서 그렇게 났다).
1666
+ *
1667
+ * 이것은 상태의 씨앗이지 계측이 아니다. 그래서 계측이 들어오는 순간 그것이 이긴다.
1668
+ */
1669
+ initialSoc: "storage.initialSoc",
1670
+ /*
1671
+ * ── 발전 — 태양광이 서 있기만 하던 자리 (2026-08-18) ────────────────────────
1672
+ *
1673
+ * 계통도에 태양광을 그려 놓고 시뮬에서는 한 톨도 만들지 못했다. 미러는 원천이 발전량을 보내 주지만,
1674
+ * 시뮬 트윈에는 그것을 만들 근거가 없었다 — 그래서 「태양광을 늘리면 피크가 얼마나 내려가나」를 물을 수
1675
+ * 없었다(what-if 의 값이 절반만 성립했다).
1676
+ *
1677
+ * **형상을 우리가 지어내지 않는다.** 위도·계절·날씨로 곡선을 만들면 그 수가 어디서 왔는지 아무도
1678
+ * 설명할 수 없고, 흐린 날 현장의 실적과 어긋난다. 그래서 현장이 **하루 형상을 적는다**: 24개 비율
1679
+ * (0~1)이면 그것이 그 현장의 곡선이다(측정한 형상을 그대로 붙일 수 있다).
1680
+ *
1681
+ * 정격만 있고 형상이 없으면 발전하지 않는다 — 하루 종일 정격으로 발전하는 태양광은 없다.
1682
+ */
1683
+ /**
1684
+ * 발전 정격(kW) — **교류 쪽 최대 출력**. 계통으로 나가는 값이다.
1685
+ *
1686
+ * 이용률의 분모가 이 값이다(만든 양 ÷ 정격 × 시간). **직류 쪽 정격(§`genRatedKWdc`)과 다르고 더 작다** —
1687
+ * 두 값을 한 이름에 담으면 이용률이 그 차이만큼 틀리고, 틀린 이유가 아무 데도 표시되지 않는다.
1688
+ *
1689
+ * 설비마다 선언하거나, 설비별 값을 모르는 현장은 **자리(수전)에 합계로** 선언한다.
1690
+ */
1691
+ genRatedKW: "generation.ratedKW",
1692
+ /**
1693
+ * 발전 정격(kW) — **직류 쪽**. 변환기(인버터)의 입력 쪽 정격이고, 태양광이면 패널 정격의 합이다.
1694
+ *
1695
+ * 이용률의 분모로 쓰지 않는다. 이 값이 교류 정격보다 큰 것은 설계이고(변환기를 그렇게 고른다),
1696
+ * 그 차이가 「맑은 정오에도 교류 출력이 더 오르지 않는 이유」를 설명한다.
1697
+ */
1698
+ genRatedKWdc: "generation.ratedKWdc",
1699
+ /**
1700
+ * 하루 형상 — 쉼표로 나눈 **24개 비율**(0~1). 시각(현장 시간대)의 정격 대비 출력이다.
1701
+ *
1702
+ * 예: `0,0,0,0,0,0,0.05,0.2,0.45,0.7,0.9,1,1,0.95,0.8,0.6,0.35,0.12,0.02,0,0,0,0,0`
1703
+ */
1704
+ genDailyProfile: "generation.dailyProfile"
1705
+ };
1706
+ var EMS_PROPERTY_SPEC = {
1707
+ [EMS_PROPERTY.contractKW]: { uom: "kW", dataType: "xs:double", note: "contracted power at the metering point, in kW." },
1708
+ /* 값이 셋 중 하나여야 한다 — 그 밖의 낱말은 받지 않는다(뜻이 통할 것 같은 말도 받지 않는다). */
1709
+ [EMS_PROPERTY.meterDirection]: {
1710
+ dataType: "xs:string",
1711
+ note: 'what this metering point measures: "import" (drawn from the grid), "export" (generation sent out), or "bidirectional" (one meter for both, sign carries the direction). Left undeclared, the point counts as load \u2014 leaving it out never makes demand look smaller than it is.'
1712
+ },
1713
+ [EMS_PROPERTY.ratedKW]: { uom: "kW", dataType: "xs:double", note: "power drawn while running, in kW." },
1714
+ [EMS_PROPERTY.standbyKW]: { uom: "kW", dataType: "xs:double", note: "power drawn while idle, in kW." },
1715
+ [EMS_PROPERTY.demandChargePerKW]: { dataType: "xs:double", note: "demand charge per kW of billing-period peak, in the declared currency." },
1716
+ [EMS_PROPERTY.billingDemandKW]: {
1717
+ uom: "kW",
1718
+ dataType: "xs:double",
1719
+ note: "billing demand in kW \u2014 the basis the demand charge is actually billed on (often a rolling 12-month peak). Not the contracted power."
1720
+ },
1721
+ [EMS_PROPERTY.energyChargePerKWh]: { dataType: "xs:double", note: "energy charge per kWh, in the declared currency." },
1722
+ [EMS_PROPERTY.currency]: { dataType: "xs:string", note: "ISO 4217 currency code, e.g. USD or KRW." },
1723
+ [EMS_PROPERTY.capacityKWh]: { uom: "kWh", dataType: "xs:double", note: "usable energy of the storage, in kWh." },
1724
+ [EMS_PROPERTY.dischargeAboveKW]: { uom: "kW", dataType: "xs:double", note: "net demand above which the storage discharges, in kW." },
1725
+ [EMS_PROPERTY.maxDischargeKW]: { uom: "kW", dataType: "xs:double", note: "inverter limit on discharge rate, in kW." },
1726
+ /* 퍼센트다 — 0.8 은 0.8% 이고 80% 가 아니다. 이 한 줄이 없어서 배터리가 조용히 비어 있었다. */
1727
+ [EMS_PROPERTY.reserveSoc]: { uom: "%", dataType: "xs:double", range: [0, 100], note: "reserve state of charge as a percentage 0-100 (20 means 20%), never discharged below." },
1728
+ [EMS_PROPERTY.initialSoc]: { uom: "%", dataType: "xs:double", range: [0, 100], note: "starting state of charge as a percentage 0-100 (80 means 80%, not 0.8)." },
1729
+ [EMS_PROPERTY.genRatedKW]: {
1730
+ uom: "kW",
1731
+ dataType: "xs:double",
1732
+ note: "rated AC generation output, in kW \u2014 the denominator of capacity factor. Declare per equipment, or on the incoming location as a site total."
1733
+ },
1734
+ [EMS_PROPERTY.genRatedKWdc]: {
1735
+ uom: "kW",
1736
+ dataType: "xs:double",
1737
+ note: "rated DC generation capacity, in kW (panel rating for PV). Larger than the AC rating by design; not the capacity-factor denominator."
1738
+ },
1739
+ [EMS_PROPERTY.genDailyProfile]: {
1740
+ dataType: "xs:string",
1741
+ note: "daily shape as 24 comma-separated fractions of rated output (0-1), one per hour of local time \u2014 the site declares its own curve; we do not invent one."
1742
+ }
1743
+ };
1744
+ function generationFractionAt(profile, hourOfDay) {
1745
+ const parts = String(profile ?? "").split(",").map((v2) => v2.trim()).filter((v2) => v2 !== "");
1746
+ if (parts.length !== 24)
1747
+ return void 0;
1748
+ const h = Math.floor(hourOfDay);
1749
+ if (!Number.isFinite(h) || h < 0 || h > 23)
1750
+ return void 0;
1751
+ const v = Number(parts[h]);
1752
+ if (!Number.isFinite(v) || v < 0 || v > 1)
1753
+ return void 0;
1754
+ return v;
1755
+ }
1756
+ var EMS_TYPES = [
1757
+ /* ── 자리: 전기적 구간 ─────────────────────────────────────────────────── */
1758
+ {
1759
+ key: "incoming",
1760
+ role: "location",
1761
+ /* 전기 계통의 구간은 **설비 계층의 단이 아니다** — 표준의 탈출구(`Other`)를 쓴다(아래 주석). */
1762
+ level: "Other",
1763
+ label: "twin.type.incoming",
1764
+ /* 수전 지점 — 계약전력이 걸리는 자리이고, 요금의 근거가 되는 수요는 여기서 잰다.
1765
+ ISO 50001 의 「에너지 유입」 경계이기도 하다(조직의 에너지 검토가 여기서 시작한다). */
1766
+ standardClass: { iec61850: "MMTR", iso50001: "EnergyInput" },
1767
+ identity: { scheme: "kernel:id" },
1768
+ capabilities: ["metered"]
1769
+ },
1770
+ {
1771
+ key: "feeder",
1772
+ role: "location",
1773
+ /* 전기 계통의 구간은 **설비 계층의 단이 아니다** — 표준의 탈출구(`Other`)를 쓴다(아래 주석). */
1774
+ level: "Other",
1775
+ label: "twin.type.feeder",
1776
+ /* 분기 회로 — 부하 분해의 단위. 계약전력의 하위 배분이 여기서 정해진다. */
1777
+ standardClass: { iec61850: "Feeder", iso50001: "EnergyUse" },
1778
+ identity: { scheme: "kernel:id" },
1779
+ capabilities: ["metered"]
1780
+ },
1781
+ {
1782
+ key: "submeter-zone",
1783
+ role: "location",
1784
+ /* 전기 계통의 구간은 **설비 계층의 단이 아니다** — 표준의 탈출구(`Other`)를 쓴다(아래 주석). */
1785
+ level: "Other",
1786
+ label: "twin.type.submeter-zone",
1787
+ /*
1788
+ * 구역 계량 — 여러 부하를 한 계량기로 묶어 재는 자리(공조·조명처럼 개별 계량이 없는 것들).
1789
+ *
1790
+ * ISO 50001 의 **SEU**(유의 에너지 사용처)가 대개 이 알갱이다. 개별 설비까지 재지 못하는 현장이
1791
+ * 많고, 그것을 「모른다」로 두는 대신 **묶음으로 아는 것**이 정직하다.
1792
+ */
1793
+ standardClass: { iec61850: "MMXU", iso50001: "SEU" },
1794
+ identity: { scheme: "kernel:id" },
1795
+ capabilities: ["metered"]
1796
+ },
1797
+ /* ── 설비: 계량 지점과 에너지 자원 ──────────────────────────────────────── */
1798
+ {
1799
+ key: "meter",
1800
+ role: "equipment",
1801
+ label: "twin.type.meter",
1802
+ /* 계량기 — 측정 논리 노드(MMXU=측정단위, MMTR=적산). 자산으로도 하나다(ISO 55000). */
1803
+ standardClass: { iec61850: "MMXU", iso55000: "Asset", iso50001: "MeasurementPoint" },
1804
+ identity: { scheme: "kernel:id" },
1805
+ capabilities: ["metered", "operable"]
1806
+ },
1807
+ {
1808
+ key: "breaker",
1809
+ role: "equipment",
1810
+ label: "twin.type.breaker",
1811
+ /*
1812
+ * 차단기 — IEC 61850 `XCBR`. **우리는 이것을 조작하지 않는다**(안전 계통은 범위 밖: ems.md §1).
1813
+ * 상태를 읽어 계통 구성을 알 뿐이다.
1814
+ *
1815
+ * 능력은 `switching` 이다 — `operable` 이 아니다(2026-08-14). `operable` 이던 동안 저작면이 차단기에
1816
+ * **제어 컴포넌트**를 권했다: 우리가 하지 않기로 한 조작을 화면이 부추긴 것이다. 「위치를 읽는다」와
1817
+ * 「명령을 받는다」는 다른 능력이고, 이제 그 둘이 갈려 있다(capability.ts 주석).
1818
+ */
1819
+ standardClass: { iec61850: "XCBR", iso55000: "Asset" },
1820
+ identity: { scheme: "kernel:id" },
1821
+ capabilities: ["switching"]
1822
+ },
1823
+ {
1824
+ key: "pv-array",
1825
+ role: "equipment",
1826
+ label: "twin.type.pv-array",
1827
+ /* 태양광 어레이 — IEC 61850-7-420(분산자원)의 `DPVA`. 발전과 계량은 다른 능력이다. */
1828
+ standardClass: { iec61850: "DPVA", iso55000: "Asset", iso50001: "RenewableSupply" },
1829
+ identity: { scheme: "kernel:id" },
1830
+ capabilities: ["energyGenerating", "metered", "operable"]
1831
+ },
1832
+ {
1833
+ key: "battery",
1834
+ role: "equipment",
1835
+ label: "twin.type.battery",
1836
+ /* 축전지(ESS) — `ZBAT`. 충전·방전을 나눠 재고, 저장은 보관(`storable`)이 아니다(물건이 아니다). */
1837
+ standardClass: { iec61850: "ZBAT", iso55000: "Asset" },
1838
+ identity: { scheme: "kernel:id" },
1839
+ capabilities: ["energyStoring", "metered", "operable"]
1840
+ },
1841
+ {
1842
+ key: "utility",
1843
+ role: "equipment",
1844
+ label: "twin.type.utility",
1845
+ /*
1846
+ * 공통 설비 — 공조·컴프레서·칠러·조명·폐수처리처럼 **어느 공정에도 귀속되지 않는** 소비처.
1847
+ *
1848
+ * ── 왜 따로 있나 (2026-08-14) ────────────────────────────────────────────
1849
+ * 물류·생산 트윈에서 이런 것들은 **설비가 아니다**(공정에 매핑되지 않으므로 자원 축에 없다).
1850
+ * 그런데 에너지에서는 소비의 절반을 차지하고 감축 후보 1순위다 — 담을 자리가 반드시 있어야 한다.
1851
+ *
1852
+ * 처음에는 `curtailable-load` 하나로 받으려 했다. 그런데 그 이름은 **「줄일 수 있다」고 주장**한다:
1853
+ * 폐수처리·방폭 환기·서버실 냉방은 공통이지만 줄일 수 없고, 그것을 감축 가능으로 두면 트윈이
1854
+ * 「이걸 줄이면 됩니다」라는 거짓 제안을 한다. 공통성과 감축 가능성은 **다른 축**이다.
1855
+ *
1856
+ * 표준: IEC 61850 에 「공통 설비」라는 논리 노드는 없다(설비 종류마다 다른 노드다) — 비운다.
1857
+ * ISO 50001 의 SEU 는 이 부류를 가장 많이 가리킨다(유의 에너지 사용처).
1858
+ */
1859
+ standardClass: { iso50001: "SEU", iso55000: "Asset" },
1860
+ identity: { scheme: "kernel:id" },
1861
+ capabilities: ["metered", "operable"]
1862
+ },
1863
+ {
1864
+ key: "curtailable-load",
1865
+ role: "equipment",
1866
+ label: "twin.type.curtailable-load",
1867
+ /*
1868
+ * 감축 가능 부하 — 줄일 수 있는 소비처(공조·충전기·비상시 미가동 라인).
1869
+ *
1870
+ * 표준에 이 이름은 없다: IEC 61850 은 설비를 종류로 부르고 「감축 가능」은 **운영 정책**이다.
1871
+ * 그래서 `iec61850` 칸을 비우고 ISO 50001 의 SEU 로만 대응한다 — 억지로 논리 노드를 적으면
1872
+ * 적합성 표가 거짓을 말한다.
1873
+ */
1874
+ standardClass: { iso50001: "SEU" },
1875
+ identity: { scheme: "kernel:id" },
1876
+ capabilities: ["curtailable", "metered", "operable"]
1877
+ }
1878
+ ];
1879
+ function electricalUpstreamOf(locations, id) {
1880
+ const byId2 = new Map((locations ?? []).filter((l) => l?.id).map((l) => [String(l.id), l]));
1881
+ const self = byId2.get(String(id));
1882
+ if (!self)
1883
+ return void 0;
1884
+ if (self.upstreamId)
1885
+ return String(self.upstreamId);
1886
+ const parent = self.parentId ? byId2.get(String(self.parentId)) : void 0;
1887
+ if (!parent)
1888
+ return void 0;
1889
+ return isElectricalLocationType(String(parent.type ?? "")) ? String(parent.id) : void 0;
1890
+ }
1891
+ function isElectricalLocationType(type) {
1892
+ return EMS_LOCATION_TYPES.includes(type);
1893
+ }
1894
+
1895
+ // ../ops-contract/dist/domain-catalog.js
1896
+ var locationKeys = (types) => types.filter((t) => t.role === "location").map((t) => t.key);
1897
+ var DOMAIN_CATALOG = {
1898
+ // label 은 언어 중립 i18n 키(twin.system.<code>) — 사람 언어는 표현계층이 렌더(L2).
1899
+ wms: { system: "wms", label: "twin.system.wms", types: WMS_TYPES, locationTypes: locationKeys(WMS_TYPES) },
1900
+ yms: { system: "yms", label: "twin.system.yms", types: YMS_TYPES, locationTypes: locationKeys(YMS_TYPES) },
1901
+ mes: { system: "mes", label: "twin.system.mes", types: MES_TYPES, locationTypes: locationKeys(MES_TYPES) },
1902
+ ems: { system: "ems", label: "twin.system.ems", types: EMS_TYPES, locationTypes: locationKeys(EMS_TYPES) }
1903
+ };
1904
+
1905
+ // ../ops-contract/dist/domain-definition.js
1906
+ var OP_PARAM = {
1907
+ /** 양품률(0..1, 무차원). 없으면 커널 기본값 — 기본값을 쓴 사실은 `specCoverage()` 가 밝힌다. */
1908
+ yield: "yield",
1909
+ /** 셋업·체인지오버 소요(ISO 8601 기간 문자열). ISA-95 는 셋업을 별도 세그먼트로도 표현하지만,
1910
+ * 현재 커널은 작업에 붙는 셋업으로 다루므로 모수로 받는다. */
1911
+ setupDuration: "setupDuration"
1912
+ };
1913
+ function procedureViolations(route) {
1914
+ const elements = route.procedure ?? [];
1915
+ if (!elements.length)
1916
+ return [];
1917
+ const errors = [];
1918
+ const byKey = new Map(elements.map((e) => [e.key, e]));
1919
+ const hasChild = new Set(elements.map((e) => e.parent).filter((k) => !!k));
1920
+ const stepSet = new Set(route.steps ?? []);
1921
+ const seenStep = /* @__PURE__ */ new Map();
1922
+ for (const n of elements) {
1923
+ if (n.parent && !byKey.has(n.parent))
1924
+ errors.push(`${n.key}: \uC0C1\uC704 '${n.parent}' \uAC00 \uC774 \uC808\uCC28\uC5D0 \uC5C6\uB2E4`);
1925
+ if (n.step === void 0)
1926
+ continue;
1927
+ if (hasChild.has(n.key))
1928
+ errors.push(`${n.key}: \uC544\uB798 \uC694\uC18C\uAC00 \uC788\uB294\uB370 \uB2E8\uACC4\uB97C \uB4E0\uB2E4 \u2014 \uC2E4\uD589\uB418\uB294 \uAC83\uC740 \uC78E\uBFD0\uC774\uB2E4`);
1929
+ if (!stepSet.has(n.step))
1930
+ errors.push(`${n.key}: \uBAA8\uB974\uB294 \uB2E8\uACC4 '${n.step}' \u2014 steps \uC5D0 \uC5C6\uB2E4`);
1931
+ const already = seenStep.get(n.step);
1932
+ if (already)
1933
+ errors.push(`\uB2E8\uACC4 '${n.step}' \uAC00 '${already}' \uC640 '${n.key}' \uB458\uC5D0 \uC2E4\uB838\uB2E4 \u2014 \uC808\uCC28 \uC5B4\uB514\uC778\uC9C0 \uB9D0\uD560 \uC218 \uC5C6\uB2E4`);
1934
+ else
1935
+ seenStep.set(n.step, n.key);
1936
+ }
1937
+ return errors;
1938
+ }
1939
+
1940
+ // ../ops-contract/dist/energy-ingest.js
1941
+ function resolveSubject(kind, localId, opts) {
1942
+ const declared = opts.identityOf?.(kind, localId);
1943
+ if (declared)
1944
+ return { subject: declared, basis: "declared" };
1945
+ return { subject: opts.scopeId ? `${opts.scopeId}/${localId}` : localId, basis: "twin-local" };
1946
+ }
1947
+ function periodFactId(tenantId, kind, subject, from, to) {
1948
+ return [tenantId, kind, subject, from, to].join("|");
1949
+ }
1950
+
1912
1951
  // ../ops-contract/dist/order-fold.js
1913
1952
  var ORDER_FOLD_HOUR_MS = 60 * 60 * 1e3;
1914
1953
  var ORDER_FOLD_DEFAULT = Object.freeze({ afterMs: 24 * 60 * 60 * 1e3, maxTerminal: 1e3 });
@@ -6976,7 +7015,7 @@ var FlowEngine = class {
6976
7015
  this.emit(objectEvent({
6977
7016
  eventTime: this.now(),
6978
7017
  action: "OBSERVE",
6979
- bizStep: CBV_BIZSTEP.consuming,
7018
+ bizStep: PRIVATE_BIZSTEP.consuming,
6980
7019
  disposition: DISP.in_progress,
6981
7020
  epcList: [],
6982
7021
  quantityList: [{ epcClass: it.gtin ?? it.epc, quantity: left, uom: it.uom }],
@@ -6990,7 +7029,7 @@ var FlowEngine = class {
6990
7029
  }
6991
7030
  if (!whole.length) return;
6992
7031
  if (this.adoptConsumed(t, whole)) {
6993
- this.observeDisposition(whole, DISP.in_progress, CBV_BIZSTEP.consuming);
7032
+ this.observeDisposition(whole, DISP.in_progress, PRIVATE_BIZSTEP.consuming);
6994
7033
  return;
6995
7034
  }
6996
7035
  const at = this.items.get(whole[0])?.location ?? t.toNode;
@@ -7005,7 +7044,7 @@ var FlowEngine = class {
7005
7044
  objectEvent({
7006
7045
  eventTime: this.now(),
7007
7046
  action: "DELETE",
7008
- bizStep: CBV_BIZSTEP.consuming,
7047
+ bizStep: PRIVATE_BIZSTEP.consuming,
7009
7048
  disposition: DISP.in_progress,
7010
7049
  epcList: whole.slice(),
7011
7050
  readPoint: at,
@@ -8065,8 +8104,8 @@ var YmsKernel = class extends FlowEngine {
8065
8104
  return;
8066
8105
  }
8067
8106
  if (to.type === "yard-slot") {
8068
- trailer.disposition = DISP.sellable;
8069
- this.emit(objectEvent({ eventTime: this.now(), action: "OBSERVE", bizStep: YARD_BIZSTEP.staging, disposition: DISP.sellable, epcList: [trailer.epc], readPoint: to.id, bizLocation: to.id }));
8107
+ delete trailer.disposition;
8108
+ this.emit(objectEvent({ eventTime: this.now(), action: "OBSERVE", bizStep: YARD_PRIVATE_BIZSTEP.staging, epcList: [trailer.epc], readPoint: to.id, bizLocation: to.id }));
8070
8109
  return;
8071
8110
  }
8072
8111
  if (to.type === "dock-door") {
@@ -8080,7 +8119,7 @@ var YmsKernel = class extends FlowEngine {
8080
8119
  this.aggregate(trailer.epc, cargo, { bizStep: YARD_BIZSTEP.loading, readPoint: to.id, consume: { readPoint: staging.id, disposition: DISP.in_transit } });
8081
8120
  }
8082
8121
  this.trailerCargo.delete(trailer.epc);
8083
- trailer.disposition = DISP.sellable;
8122
+ delete trailer.disposition;
8084
8123
  } else if (cargo.length && staging) {
8085
8124
  this.disaggregate(trailer.epc, cargo, { bizStep: YARD_BIZSTEP.unloading, readPoint: to.id, materialize: { location: staging.id, disposition: DISP.sellable } });
8086
8125
  this.trailerCargo.delete(trailer.epc);