@operato/twin-kernel 0.11.25 → 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}`;
@@ -1743,173 +1340,616 @@ function lotClassUri(input) {
1743
1340
  throw new Error(`lotClassUri: ${wrong}`);
1744
1341
  return uri;
1745
1342
  }
1746
- if (!item)
1747
- throw new Error(`lotClassUri: \uD488\uBC88\uC774 \uBE44\uC5C8\uB2E4 (lot=${lot})`);
1748
- const enc = (v) => v.replace(LOT_COMPONENT_BREAKING, (c) => PERCENT_ENCODED[c]);
1749
- return joinUnderNamespace(input.namespace, "class", `${enc(item)}${LOT_SEPARATOR}${enc(lot)}`);
1750
- }
1751
- var LOT_SEPARATOR = ";lot=";
1752
- var LOT_COMPONENT_BREAKING = /[%/:?# ;]/g;
1753
- function bizTransactionUri(namespace, transId) {
1754
- return underNamespace(namespace, "bt", transId);
1755
- }
1756
- var STRUCTURE_BREAKING = /[%/:?# ]/g;
1757
- var PERCENT_ENCODED = {
1758
- "%": "%25",
1759
- "/": "%2F",
1760
- ":": "%3A",
1761
- "?": "%3F",
1762
- "#": "%23",
1763
- " ": "%20",
1764
- /* 로트 클래스의 가르는 글자 — `lotClassUri` 만 쓴다(§`LOT_COMPONENT_BREAKING`). 표는 한 벌이다. */
1765
- ";": "%3B"
1766
- };
1767
- function underNamespace(namespace, marker, id) {
1768
- const raw = String(id);
1769
- if (!raw)
1770
- return void 0;
1771
- return joinUnderNamespace(namespace, marker, raw.replace(STRUCTURE_BREAKING, (c) => PERCENT_ENCODED[c]));
1772
- }
1773
- function joinUnderNamespace(namespace, marker, v) {
1774
- const ns = namespace?.trim();
1775
- if (!ns)
1343
+ if (!item)
1344
+ throw new Error(`lotClassUri: \uD488\uBC88\uC774 \uBE44\uC5C8\uB2E4 (lot=${lot})`);
1345
+ const enc = (v) => v.replace(LOT_COMPONENT_BREAKING, (c) => PERCENT_ENCODED[c]);
1346
+ return joinUnderNamespace(input.namespace, "class", `${enc(item)}${LOT_SEPARATOR}${enc(lot)}`);
1347
+ }
1348
+ var LOT_SEPARATOR = ";lot=";
1349
+ var LOT_COMPONENT_BREAKING = /[%/:?# ;]/g;
1350
+ function bizTransactionUri(namespace, transId) {
1351
+ return underNamespace(namespace, "bt", transId);
1352
+ }
1353
+ var STRUCTURE_BREAKING = /[%/:?# ]/g;
1354
+ var PERCENT_ENCODED = {
1355
+ "%": "%25",
1356
+ "/": "%2F",
1357
+ ":": "%3A",
1358
+ "?": "%3F",
1359
+ "#": "%23",
1360
+ " ": "%20",
1361
+ /* 로트 클래스의 가르는 글자 — `lotClassUri` 만 쓴다(§`LOT_COMPONENT_BREAKING`). 표는 한 벌이다. */
1362
+ ";": "%3B"
1363
+ };
1364
+ function underNamespace(namespace, marker, id) {
1365
+ const raw = String(id);
1366
+ if (!raw)
1367
+ return void 0;
1368
+ return joinUnderNamespace(namespace, marker, raw.replace(STRUCTURE_BREAKING, (c) => PERCENT_ENCODED[c]));
1369
+ }
1370
+ function joinUnderNamespace(namespace, marker, v) {
1371
+ const ns = namespace?.trim();
1372
+ if (!ns)
1373
+ return void 0;
1374
+ if (/^https?:\/\/[^/\s]+/.test(ns))
1375
+ return `${ns.replace(/\/+$/, "")}/${marker}/${v}`;
1376
+ if (/^urn:epc(global)?:/.test(ns))
1377
+ return void 0;
1378
+ if (/^urn:[^:\s]+/.test(ns))
1379
+ return `${ns.replace(/:+$/, "")}:${marker}:${v}`;
1380
+ return void 0;
1381
+ }
1382
+ function header(type, eventTime, bizStep, opts) {
1383
+ const h = {
1384
+ "@context": EPCIS_CONTEXT,
1385
+ type,
1386
+ eventTime,
1387
+ eventTimeZoneOffset: UTC_OFFSET,
1388
+ bizStep
1389
+ };
1390
+ if (opts?.eventID)
1391
+ h.eventID = opts.eventID;
1392
+ if (opts?.recordTime)
1393
+ h.recordTime = opts.recordTime;
1394
+ if (opts?.errorDeclaration)
1395
+ h.errorDeclaration = opts.errorDeclaration;
1396
+ if (opts?.bizTransactionList?.length)
1397
+ h.bizTransactionList = opts.bizTransactionList;
1398
+ if (opts?.ilmd)
1399
+ h.ilmd = opts.ilmd;
1400
+ if (opts?.sourceList)
1401
+ h.sourceList = opts.sourceList;
1402
+ if (opts?.destinationList)
1403
+ h.destinationList = opts.destinationList;
1404
+ if (opts?.persistentDisposition)
1405
+ h.persistentDisposition = opts.persistentDisposition;
1406
+ if (opts?.sensorElementList)
1407
+ h.sensorElementList = opts.sensorElementList;
1408
+ if (opts?.certificationInfo)
1409
+ h.certificationInfo = opts.certificationInfo;
1410
+ return h;
1411
+ }
1412
+ function common(type, eventTime, action, bizStep, opts) {
1413
+ return { ...header(type, eventTime, bizStep, opts), action };
1414
+ }
1415
+ function objectEvent(p) {
1416
+ const e = { ...common("ObjectEvent", p.eventTime, p.action, p.bizStep, p), epcList: p.epcList };
1417
+ if (p.disposition)
1418
+ e.disposition = p.disposition;
1419
+ if (p.quantityList)
1420
+ e.quantityList = p.quantityList;
1421
+ if (p.readPoint)
1422
+ e.readPoint = { id: p.readPoint };
1423
+ if (p.bizLocation)
1424
+ e.bizLocation = { id: p.bizLocation };
1425
+ if (p.bizTransactionList)
1426
+ e.bizTransactionList = p.bizTransactionList;
1427
+ return e;
1428
+ }
1429
+ function aggregationEvent(p) {
1430
+ const e = { ...common("AggregationEvent", p.eventTime, p.action, p.bizStep, p), parentID: p.parentID };
1431
+ if (p.disposition)
1432
+ e.disposition = p.disposition;
1433
+ if (p.childEPCs)
1434
+ e.childEPCs = p.childEPCs;
1435
+ if (p.childQuantityList)
1436
+ e.childQuantityList = p.childQuantityList;
1437
+ if (p.readPoint)
1438
+ e.readPoint = { id: p.readPoint };
1439
+ if (p.bizLocation)
1440
+ e.bizLocation = { id: p.bizLocation };
1441
+ return e;
1442
+ }
1443
+ function transactionEvent(p) {
1444
+ const e = {
1445
+ ...common("TransactionEvent", p.eventTime, p.action, p.bizStep, p),
1446
+ bizTransactionList: p.bizTransactionList
1447
+ };
1448
+ if (p.disposition)
1449
+ e.disposition = p.disposition;
1450
+ if (p.parentID)
1451
+ e.parentID = p.parentID;
1452
+ if (p.epcList)
1453
+ e.epcList = p.epcList;
1454
+ if (p.quantityList)
1455
+ e.quantityList = p.quantityList;
1456
+ if (p.readPoint)
1457
+ e.readPoint = { id: p.readPoint };
1458
+ if (p.bizLocation)
1459
+ e.bizLocation = { id: p.bizLocation };
1460
+ return e;
1461
+ }
1462
+ function transformationEvent(p) {
1463
+ const e = header("TransformationEvent", p.eventTime, p.bizStep, p);
1464
+ if (p.disposition)
1465
+ e.disposition = p.disposition;
1466
+ if (p.inputEPCList)
1467
+ e.inputEPCList = p.inputEPCList;
1468
+ if (p.inputQuantityList)
1469
+ e.inputQuantityList = p.inputQuantityList;
1470
+ if (p.outputEPCList)
1471
+ e.outputEPCList = p.outputEPCList;
1472
+ if (p.outputQuantityList)
1473
+ e.outputQuantityList = p.outputQuantityList;
1474
+ if (p.transformationID)
1475
+ e.transformationID = p.transformationID;
1476
+ if (p.readPoint)
1477
+ e.readPoint = { id: p.readPoint };
1478
+ if (p.bizLocation)
1479
+ e.bizLocation = { id: p.bizLocation };
1480
+ if (p.bizTransactionList)
1481
+ e.bizTransactionList = p.bizTransactionList;
1482
+ return e;
1483
+ }
1484
+ var GS1_KEY_DIGITS = { sgtin: 13, lgtin: 13, sscc: 17, grai: 12, gdti: 12, sgln: 12 };
1485
+ function gs1KeyDigitViolation(uri) {
1486
+ if (!uri)
1487
+ return void 0;
1488
+ const m = /^urn:epc:(?:id|idpat|class):([a-z]+):([^:]+)$/.exec(uri);
1489
+ if (!m)
1490
+ return void 0;
1491
+ const want = GS1_KEY_DIGITS[m[1]];
1492
+ if (!want)
1493
+ return void 0;
1494
+ const parts = m[2].split(".");
1495
+ if (parts.length < 2)
1496
+ return `${m[1]} \uD615\uC2DD \uC624\uB958: ${uri} \u2014 \uD68C\uC0AC \uD504\uB9AC\uD53D\uC2A4\uC640 \uCC38\uC870\uAC00 '.' \uB85C \uAC08\uB824\uC57C \uD55C\uB2E4`;
1497
+ const [prefix, ref] = parts;
1498
+ if (ref === "*")
1499
+ return void 0;
1500
+ if (!/^\d+$/.test(prefix) || !/^\d+$/.test(ref)) {
1501
+ return `${m[1]} \uD615\uC2DD \uC624\uB958: ${uri} \u2014 \uD68C\uC0AC \uD504\uB9AC\uD53D\uC2A4\uC640 \uCC38\uC870\uB294 \uC22B\uC790\uB2E4`;
1502
+ }
1503
+ const got = prefix.length + ref.length;
1504
+ if (got === want)
1505
+ return void 0;
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.`;
1507
+ }
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)
1776
1883
  return void 0;
1777
- if (/^https?:\/\/[^/\s]+/.test(ns))
1778
- return `${ns.replace(/\/+$/, "")}/${marker}/${v}`;
1779
- if (/^urn:epc(global)?:/.test(ns))
1884
+ if (self.upstreamId)
1885
+ return String(self.upstreamId);
1886
+ const parent = self.parentId ? byId2.get(String(self.parentId)) : void 0;
1887
+ if (!parent)
1780
1888
  return void 0;
1781
- if (/^urn:[^:\s]+/.test(ns))
1782
- return `${ns.replace(/:+$/, "")}:${marker}:${v}`;
1783
- return void 0;
1784
- }
1785
- function header(type, eventTime, bizStep, opts) {
1786
- const h = {
1787
- "@context": EPCIS_CONTEXT,
1788
- type,
1789
- eventTime,
1790
- eventTimeZoneOffset: UTC_OFFSET,
1791
- bizStep
1792
- };
1793
- if (opts?.eventID)
1794
- h.eventID = opts.eventID;
1795
- if (opts?.recordTime)
1796
- h.recordTime = opts.recordTime;
1797
- if (opts?.errorDeclaration)
1798
- h.errorDeclaration = opts.errorDeclaration;
1799
- if (opts?.bizTransactionList?.length)
1800
- h.bizTransactionList = opts.bizTransactionList;
1801
- if (opts?.ilmd)
1802
- h.ilmd = opts.ilmd;
1803
- if (opts?.sourceList)
1804
- h.sourceList = opts.sourceList;
1805
- if (opts?.destinationList)
1806
- h.destinationList = opts.destinationList;
1807
- if (opts?.persistentDisposition)
1808
- h.persistentDisposition = opts.persistentDisposition;
1809
- if (opts?.sensorElementList)
1810
- h.sensorElementList = opts.sensorElementList;
1811
- if (opts?.certificationInfo)
1812
- h.certificationInfo = opts.certificationInfo;
1813
- return h;
1814
- }
1815
- function common(type, eventTime, action, bizStep, opts) {
1816
- return { ...header(type, eventTime, bizStep, opts), action };
1817
- }
1818
- function objectEvent(p) {
1819
- const e = { ...common("ObjectEvent", p.eventTime, p.action, p.bizStep, p), epcList: p.epcList };
1820
- if (p.disposition)
1821
- e.disposition = p.disposition;
1822
- if (p.quantityList)
1823
- e.quantityList = p.quantityList;
1824
- if (p.readPoint)
1825
- e.readPoint = { id: p.readPoint };
1826
- if (p.bizLocation)
1827
- e.bizLocation = { id: p.bizLocation };
1828
- if (p.bizTransactionList)
1829
- e.bizTransactionList = p.bizTransactionList;
1830
- return e;
1889
+ return isElectricalLocationType(String(parent.type ?? "")) ? String(parent.id) : void 0;
1831
1890
  }
1832
- function aggregationEvent(p) {
1833
- const e = { ...common("AggregationEvent", p.eventTime, p.action, p.bizStep, p), parentID: p.parentID };
1834
- if (p.disposition)
1835
- e.disposition = p.disposition;
1836
- if (p.childEPCs)
1837
- e.childEPCs = p.childEPCs;
1838
- if (p.childQuantityList)
1839
- e.childQuantityList = p.childQuantityList;
1840
- if (p.readPoint)
1841
- e.readPoint = { id: p.readPoint };
1842
- if (p.bizLocation)
1843
- e.bizLocation = { id: p.bizLocation };
1844
- return e;
1891
+ function isElectricalLocationType(type) {
1892
+ return EMS_LOCATION_TYPES.includes(type);
1845
1893
  }
1846
- function transactionEvent(p) {
1847
- const e = {
1848
- ...common("TransactionEvent", p.eventTime, p.action, p.bizStep, p),
1849
- bizTransactionList: p.bizTransactionList
1850
- };
1851
- if (p.disposition)
1852
- e.disposition = p.disposition;
1853
- if (p.parentID)
1854
- e.parentID = p.parentID;
1855
- if (p.epcList)
1856
- e.epcList = p.epcList;
1857
- if (p.quantityList)
1858
- e.quantityList = p.quantityList;
1859
- if (p.readPoint)
1860
- e.readPoint = { id: p.readPoint };
1861
- if (p.bizLocation)
1862
- e.bizLocation = { id: p.bizLocation };
1863
- return e;
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;
1864
1938
  }
1865
- function transformationEvent(p) {
1866
- const e = header("TransformationEvent", p.eventTime, p.bizStep, p);
1867
- if (p.disposition)
1868
- e.disposition = p.disposition;
1869
- if (p.inputEPCList)
1870
- e.inputEPCList = p.inputEPCList;
1871
- if (p.inputQuantityList)
1872
- e.inputQuantityList = p.inputQuantityList;
1873
- if (p.outputEPCList)
1874
- e.outputEPCList = p.outputEPCList;
1875
- if (p.outputQuantityList)
1876
- e.outputQuantityList = p.outputQuantityList;
1877
- if (p.transformationID)
1878
- e.transformationID = p.transformationID;
1879
- if (p.readPoint)
1880
- e.readPoint = { id: p.readPoint };
1881
- if (p.bizLocation)
1882
- e.bizLocation = { id: p.bizLocation };
1883
- if (p.bizTransactionList)
1884
- e.bizTransactionList = p.bizTransactionList;
1885
- return e;
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" };
1886
1946
  }
1887
- var GS1_KEY_DIGITS = { sgtin: 13, lgtin: 13, sscc: 17, grai: 12, gdti: 12, sgln: 12 };
1888
- function gs1KeyDigitViolation(uri) {
1889
- if (!uri)
1890
- return void 0;
1891
- const m = /^urn:epc:(?:id|idpat|class):([a-z]+):([^:]+)$/.exec(uri);
1892
- if (!m)
1893
- return void 0;
1894
- const want = GS1_KEY_DIGITS[m[1]];
1895
- if (!want)
1896
- return void 0;
1897
- const parts = m[2].split(".");
1898
- if (parts.length < 2)
1899
- return `${m[1]} \uD615\uC2DD \uC624\uB958: ${uri} \u2014 \uD68C\uC0AC \uD504\uB9AC\uD53D\uC2A4\uC640 \uCC38\uC870\uAC00 '.' \uB85C \uAC08\uB824\uC57C \uD55C\uB2E4`;
1900
- const [prefix, ref] = parts;
1901
- if (ref === "*")
1902
- return void 0;
1903
- if (!/^\d+$/.test(prefix) || !/^\d+$/.test(ref)) {
1904
- return `${m[1]} \uD615\uC2DD \uC624\uB958: ${uri} \u2014 \uD68C\uC0AC \uD504\uB9AC\uD53D\uC2A4\uC640 \uCC38\uC870\uB294 \uC22B\uC790\uB2E4`;
1905
- }
1906
- const got = prefix.length + ref.length;
1907
- if (got === want)
1908
- return void 0;
1909
- 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.`;
1947
+ function periodFactId(tenantId, kind, subject, from, to) {
1948
+ return [tenantId, kind, subject, from, to].join("|");
1910
1949
  }
1911
1950
 
1912
1951
  // ../ops-contract/dist/order-fold.js
1952
+ var ORDER_FOLD_HOUR_MS = 60 * 60 * 1e3;
1913
1953
  var ORDER_FOLD_DEFAULT = Object.freeze({ afterMs: 24 * 60 * 60 * 1e3, maxTerminal: 1e3 });
1914
1954
  function orderFoldPolicyOf(declared) {
1915
1955
  const policy = { ...ORDER_FOLD_DEFAULT };
@@ -1922,6 +1962,9 @@ function orderFoldPolicyOf(declared) {
1922
1962
  }
1923
1963
  policy[key] = key === "maxTerminal" ? Math.floor(v) : v;
1924
1964
  }
1965
+ if (policy.afterMs % ORDER_FOLD_HOUR_MS !== 0) {
1966
+ return { error: `order fold afterMs must be a whole number of hours, got ${policy.afterMs} ms \u2014 the window is counted in hour slots (ADR-0092)` };
1967
+ }
1925
1968
  if (policy.maxTerminal < 1)
1926
1969
  return { error: "order fold maxTerminal must be at least 1 (ADR-0092)" };
1927
1970
  return { policy };
@@ -1949,6 +1992,7 @@ function foldOrdersInto(folded, orders) {
1949
1992
  counts: Object.fromEntries(Object.entries(folded?.counts ?? {}).map(([k, v]) => [k, { ...v }])),
1950
1993
  boundaryMs: folded?.boundaryMs ?? null
1951
1994
  };
1995
+ const hours = Object.fromEntries(Object.entries(folded?.hours ?? {}).map(([h, byKind]) => [h, Object.fromEntries(Object.entries(byKind).map(([k, v]) => [k, { ...v }]))]));
1952
1996
  for (const o of orders) {
1953
1997
  const kind = String(o.kind ?? "");
1954
1998
  const status = String(o.status ?? "");
@@ -1956,8 +2000,12 @@ function foldOrdersInto(folded, orders) {
1956
2000
  byStatus[status] = (byStatus[status] ?? 0) + 1;
1957
2001
  if (typeof o.terminalAtMs === "number" && Number.isFinite(o.terminalAtMs)) {
1958
2002
  out.boundaryMs = out.boundaryMs === null ? o.terminalAtMs : Math.max(out.boundaryMs, o.terminalAtMs);
2003
+ const slot = (hours[String(hourStartOf(o.terminalAtMs))] ??= {})[kind] ??= {};
2004
+ slot[status] = (slot[status] ?? 0) + 1;
1959
2005
  }
1960
2006
  }
2007
+ if (Object.keys(hours).length)
2008
+ out.hours = hours;
1961
2009
  return out;
1962
2010
  }
1963
2011
  function standsNewOrderRow(record, eventTimeMs, folded) {
@@ -1970,6 +2018,22 @@ function standsNewOrderRow(record, eventTimeMs, folded) {
1970
2018
  return true;
1971
2019
  return eventTimeMs > boundary;
1972
2020
  }
2021
+ function hourStartOf(ms2) {
2022
+ return Math.floor(ms2 / ORDER_FOLD_HOUR_MS) * ORDER_FOLD_HOUR_MS;
2023
+ }
2024
+ function finishedWindowStartMs(nowMs, policy = ORDER_FOLD_DEFAULT) {
2025
+ return hourStartOf(nowMs) - policy.afterMs;
2026
+ }
2027
+ function pruneFoldedHours(folded, nowMs, policy = ORDER_FOLD_DEFAULT) {
2028
+ if (!folded.hours)
2029
+ return folded;
2030
+ const start = finishedWindowStartMs(nowMs, policy);
2031
+ const kept = Object.fromEntries(Object.entries(folded.hours).filter(([h]) => Number(h) >= start));
2032
+ const out = { counts: folded.counts, boundaryMs: folded.boundaryMs };
2033
+ if (Object.keys(kept).length)
2034
+ out.hours = kept;
2035
+ return out;
2036
+ }
1973
2037
 
1974
2038
  // ../ops-contract/dist/iso-duration.js
1975
2039
  var RE = /^(-)?P(?:(\d+(?:\.\d+)?)W)?(?:(\d+(?:\.\d+)?)D)?(?:T(?:(\d+(?:\.\d+)?)H)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)S)?)?$/;
@@ -3852,7 +3916,7 @@ var ObservedReducer = class {
3852
3916
  const ids = planOrderFold([...this.orders.values()], nowMs, this.orderFoldPolicy);
3853
3917
  if (!ids.length) return 0;
3854
3918
  const rows = ids.map((id) => this.orders.get(id)).filter(Boolean);
3855
- this.foldedOrders = foldOrdersInto(this.foldedOrders, rows);
3919
+ this.foldedOrders = pruneFoldedHours(foldOrdersInto(this.foldedOrders, rows), nowMs, this.orderFoldPolicy);
3856
3920
  for (const id of ids) {
3857
3921
  this.orders.delete(id);
3858
3922
  this.lastAt.delete(`order:${id}`);
@@ -4524,7 +4588,8 @@ var FlowEngine = class {
4524
4588
  }
4525
4589
  const ids = planOrderFold([...this.orders.values()], nowMs ?? this.nowMs(), this.orderFoldPolicy);
4526
4590
  if (!ids.length) return 0;
4527
- this.foldedOrders = foldOrdersInto(this.foldedOrders, ids.map((id) => this.orders.get(id)).filter(Boolean));
4591
+ const now = nowMs ?? this.nowMs();
4592
+ this.foldedOrders = pruneFoldedHours(foldOrdersInto(this.foldedOrders, ids.map((id) => this.orders.get(id)).filter(Boolean)), now, this.orderFoldPolicy);
4528
4593
  for (const id of ids) this.orders.delete(id);
4529
4594
  this.revision++;
4530
4595
  return ids.length;
@@ -6950,7 +7015,7 @@ var FlowEngine = class {
6950
7015
  this.emit(objectEvent({
6951
7016
  eventTime: this.now(),
6952
7017
  action: "OBSERVE",
6953
- bizStep: CBV_BIZSTEP.consuming,
7018
+ bizStep: PRIVATE_BIZSTEP.consuming,
6954
7019
  disposition: DISP.in_progress,
6955
7020
  epcList: [],
6956
7021
  quantityList: [{ epcClass: it.gtin ?? it.epc, quantity: left, uom: it.uom }],
@@ -6964,7 +7029,7 @@ var FlowEngine = class {
6964
7029
  }
6965
7030
  if (!whole.length) return;
6966
7031
  if (this.adoptConsumed(t, whole)) {
6967
- this.observeDisposition(whole, DISP.in_progress, CBV_BIZSTEP.consuming);
7032
+ this.observeDisposition(whole, DISP.in_progress, PRIVATE_BIZSTEP.consuming);
6968
7033
  return;
6969
7034
  }
6970
7035
  const at = this.items.get(whole[0])?.location ?? t.toNode;
@@ -6979,7 +7044,7 @@ var FlowEngine = class {
6979
7044
  objectEvent({
6980
7045
  eventTime: this.now(),
6981
7046
  action: "DELETE",
6982
- bizStep: CBV_BIZSTEP.consuming,
7047
+ bizStep: PRIVATE_BIZSTEP.consuming,
6983
7048
  disposition: DISP.in_progress,
6984
7049
  epcList: whole.slice(),
6985
7050
  readPoint: at,
@@ -8039,8 +8104,8 @@ var YmsKernel = class extends FlowEngine {
8039
8104
  return;
8040
8105
  }
8041
8106
  if (to.type === "yard-slot") {
8042
- trailer.disposition = DISP.sellable;
8043
- 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 }));
8044
8109
  return;
8045
8110
  }
8046
8111
  if (to.type === "dock-door") {
@@ -8054,7 +8119,7 @@ var YmsKernel = class extends FlowEngine {
8054
8119
  this.aggregate(trailer.epc, cargo, { bizStep: YARD_BIZSTEP.loading, readPoint: to.id, consume: { readPoint: staging.id, disposition: DISP.in_transit } });
8055
8120
  }
8056
8121
  this.trailerCargo.delete(trailer.epc);
8057
- trailer.disposition = DISP.sellable;
8122
+ delete trailer.disposition;
8058
8123
  } else if (cargo.length && staging) {
8059
8124
  this.disaggregate(trailer.epc, cargo, { bizStep: YARD_BIZSTEP.unloading, readPoint: to.id, materialize: { location: staging.id, disposition: DISP.sellable } });
8060
8125
  this.trailerCargo.delete(trailer.epc);