@operato/ops-contract 0.1.0

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.
Files changed (41) hide show
  1. package/README.md +23 -0
  2. package/dist/capability.d.ts +142 -0
  3. package/dist/capability.js +127 -0
  4. package/dist/capacity.d.ts +99 -0
  5. package/dist/capacity.js +172 -0
  6. package/dist/contract.d.ts +3557 -0
  7. package/dist/contract.js +1248 -0
  8. package/dist/domain-catalog.d.ts +280 -0
  9. package/dist/domain-catalog.js +322 -0
  10. package/dist/domain-definition.d.ts +356 -0
  11. package/dist/domain-definition.js +137 -0
  12. package/dist/ems-profile.d.ts +147 -0
  13. package/dist/ems-profile.js +367 -0
  14. package/dist/energy-ingest.d.ts +214 -0
  15. package/dist/energy-ingest.js +801 -0
  16. package/dist/epcis.d.ts +458 -0
  17. package/dist/epcis.js +640 -0
  18. package/dist/face2-adapter.d.ts +191 -0
  19. package/dist/face2-adapter.js +284 -0
  20. package/dist/index.d.ts +18 -0
  21. package/dist/index.js +39 -0
  22. package/dist/iso-duration.d.ts +5 -0
  23. package/dist/iso-duration.js +43 -0
  24. package/dist/master-data.d.ts +46 -0
  25. package/dist/master-data.js +100 -0
  26. package/dist/mes-profile.d.ts +14 -0
  27. package/dist/mes-profile.js +59 -0
  28. package/dist/operational-ingest.d.ts +44 -0
  29. package/dist/operational-ingest.js +379 -0
  30. package/dist/operations-capability.d.ts +117 -0
  31. package/dist/operations-capability.js +120 -0
  32. package/dist/scenario-validate.d.ts +15 -0
  33. package/dist/scenario-validate.js +72 -0
  34. package/dist/vocabulary.d.ts +28 -0
  35. package/dist/vocabulary.js +81 -0
  36. package/dist/wms-profile.d.ts +20 -0
  37. package/dist/wms-profile.js +58 -0
  38. package/dist/yms-profile.d.ts +15 -0
  39. package/dist/yms-profile.js +39 -0
  40. package/dist-cjs/index.cjs +3767 -0
  41. package/package.json +30 -0
@@ -0,0 +1,356 @@
1
+ /** 표준 온톨로지 투영(열린 문자열 — 하드코딩 enum 금지). */
2
+ /**
3
+ * 표준 온톨로지 투영 — 이 개념을 각 표준이 무엇이라 부르나.
4
+ *
5
+ * ── 왜 키가 늘었나 (2026-08-14) ────────────────────────────────────────────
6
+ * 셋(`epcis`·`isa95`·`iso55000`)으로 고정돼 있었다. 물류·생산·자산은 그 셋으로 덮이지만
7
+ * **에너지는 덮이지 않는다** — 계측·설비 모델은 IEC 61850, 성과·관리체계는 ISO 50001 이다.
8
+ * 자리가 없으면 대응을 **적을 수 없고**, 적을 수 없으면 적합성 표가 그 개념을 「표준에 없음」으로
9
+ * 센다 — 없는 것이 아니라 **다른 표준의 것**인데도.
10
+ *
11
+ * 값은 열린 문자열이다(하드코딩 enum 금지). 표준에 자리가 없으면 그 키를 **비운다** —
12
+ * 억지로 가까운 이름을 적으면 적합성 표가 거짓을 말한다.
13
+ */
14
+ export interface StandardClass {
15
+ epcis?: string;
16
+ isa95?: string;
17
+ iso55000?: string;
18
+ /** ISO 50001 — 에너지 경영. 성과지표(EnPI)·기준선·유의 에너지 사용(SEU) 같은 **역할** 이름. */
19
+ iso50001?: string;
20
+ /** IEC 61850 — 전력 설비 데이터 모델. 논리 노드 이름(예: `MMXU`·`MMTR`·`XCBR`·`ZBAT`). */
21
+ iec61850?: string;
22
+ }
23
+ export interface Identity {
24
+ /** GS1/커널 식별 스킴 힌트. 예: 'gs1:SGLN' | 'gs1:GIAI' | 'gs1:SSCC'. */
25
+ scheme: string;
26
+ }
27
+ /** 자재/품목 클래스. */
28
+ export interface MaterialDef {
29
+ key: string;
30
+ label: string;
31
+ identity?: Identity;
32
+ /**
33
+ * 이 자재가 놓이는 자리 타입(`LocationTypeDef.key`) — **보관처는 자재의 성질이다.**
34
+ *
35
+ * 이 자리가 없던 동안 커널은 `'raw-store'`·`'fg-store'` 라는 이름을 **코드에 박아 두고** 있었다.
36
+ * 그래서 현장은 자기 창고를 그 이름으로 **개명해야** 트윈이 굴러갔다 — 커널이 현장의 낱말을 정하는
37
+ * 셈이고, 개명하지 않은 트윈은 조용히 멈추거나(자재가 영원히 안 들어옴) 터졌다.
38
+ *
39
+ * **왜 레시피 줄이 아니라 자재인가**: 같은 밀가루를 쓰는 레시피가 열이면 줄마다 같은 값을 열 번
40
+ * 적게 되고, 창고를 옮길 때 열 곳을 고쳐야 한다. 보관처는 제품 구조(BOM)의 성질이 아니다 —
41
+ * 실 시스템도 그렇게 두지 않는다: 조사한 식음료 MES 에서 **품목이 창고를 들고 BOM 줄에는 없었다.**
42
+ * (커널은 어느 제품에도 기대지 않는다 — 실 시스템은 이 결함이 실재한다는 **증인**이고, 커널의 모양을
43
+ * 정하는 것은 표준과 원칙이다.)
44
+ *
45
+ * ── 요구가 아니라 **정책**이다 (2026-08-22) ────────────────────────────────
46
+ * 예전에는 레시피가 쓰는 자재에 이것을 요구했다. 그런데 재고로 위치를 말하는 시스템에는 이 선언이
47
+ * 없고(그것이 WMS 계열의 정상이다), 표준도 위치를 정의가 아니라 **로트**에 둔다. 실측: 첫 실 연동에서
48
+ * 원자재 986건 중 36건만 선언돼 있어 레시피 937/1,408 이 실리지 못했다.
49
+ *
50
+ * 그래서 요구를 거두었다. **있으면** 확보 범위를 그 타입의 자리로 좁히고(현장의 정책), **없으면**
51
+ * 재고가 있는 곳에서 찾는다(품목 색인 — §`ItemStore.ofGtin`).
52
+ *
53
+ * 다만 **입고를 만들려면 자리가 필요하다** — 시뮬레이션이 자재를 내려놓을 곳을 지어낼 수는 없다.
54
+ * 선언이 없는 자재는 입고가 만들어지지 않고, 커널이 그 사실을 한 번 알린다(조용히 빠지지 않는다).
55
+ * 미러에서는 문제가 아니다: 재고는 원본이 말한다.
56
+ */
57
+ locationType?: string;
58
+ }
59
+ /** 로케이션(수동 위치) 타입. */
60
+ export interface LocationTypeDef {
61
+ key: string;
62
+ label: string;
63
+ standardClass?: StandardClass;
64
+ identity?: Identity;
65
+ capabilities?: string[];
66
+ }
67
+ /** 자원(능동 설비·설비) 타입. */
68
+ export interface ResourceTypeDef {
69
+ key: string;
70
+ label: string;
71
+ standardClass?: StandardClass;
72
+ identity?: Identity;
73
+ capabilities?: string[];
74
+ }
75
+ /** 작업 의도 — FlowTask.intent 와 정합(ISA-95 이동 vs 변환). */
76
+ export type OperationIntent = 'transport' | 'process' | 'dwell';
77
+ /**
78
+ * ISO 8601 기간 표기(예 `PT12M`·`PT1H15M`) — **ISA-95 와 같은 표기.**
79
+ * 근거: B2MML `OperationsSegmentType.Duration` 의 타입 `DurationType` = `xsd:restriction base="xsd:duration"`.
80
+ * 밀리초 숫자로 계약하지 않는 이유: 명세는 사람과 원 시스템이 주는 값이고, 표준이 이미 표기를 정해 뒀다.
81
+ */
82
+ export type IsoDuration = string;
83
+ /**
84
+ * 공정 모수 — **ISA-95 `OperationsSegment.ParameterSpecification`(`ParameterType`) 1:1.**
85
+ * 표준의 `ParameterType` = `ID` + `Value`(`ValueString` + `DataType` + `UnitOfMeasure`) 이므로 그 모양을 따른다.
86
+ *
87
+ * 표준은 **파라미터 ID 어휘를 정하지 않는다** — 상위 문서/당사자 합의 소관이다. 그래서 우리가 쓰는 ID 는
88
+ * `OP_PARAM` 한 곳에서만 정의한다(어휘를 코드 곳곳에서 발명하지 않기 위해 — ILMD_ATTR 과 같은 규율).
89
+ */
90
+ export interface OperationParameter {
91
+ /** ISA-95 `Parameter.ID`. 우리 canonical ID 는 `OP_PARAM` 참조. */
92
+ id: string;
93
+ /** ISA-95 `Value.ValueString` — 표준이 문자열로 싣는다(숫자는 소비처가 해석). */
94
+ value: string;
95
+ /** ISA-95 `Value.UnitOfMeasure`. 무차원(비율 등)이면 생략. */
96
+ uom?: string;
97
+ }
98
+ /**
99
+ * 우리가 소비하는 canonical 파라미터 ID — **표준이 이름을 정해 주지 않는 자리**이므로 여기서 한 번 정한다.
100
+ * 새 모수를 쓸 때는 반드시 여기에 등록한다(문자열 리터럴을 코드에 흩뿌리지 않는다).
101
+ */
102
+ export declare const OP_PARAM: {
103
+ /** 양품률(0..1, 무차원). 없으면 커널 기본값 — 기본값을 쓴 사실은 `specCoverage()` 가 밝힌다. */
104
+ readonly yield: "yield";
105
+ /** 셋업·체인지오버 소요(ISO 8601 기간 문자열). ISA-95 는 셋업을 별도 세그먼트로도 표현하지만,
106
+ * 현재 커널은 작업에 붙는 셋업으로 다루므로 모수로 받는다. */
107
+ readonly setupDuration: "setupDuration";
108
+ };
109
+ /**
110
+ * 소요시간 변동 — **표준 밖 확장이며, 그렇게 표시한다.**
111
+ * ISA-95 `Duration` 은 스칼라 하나라 분포를 담지 못한다. 그런데 시뮬레이션의 신뢰도는 분산에서 나온다
112
+ * (평균만 맞는 상수 모델은 대기·병목을 구조적으로 과소평가한다). 그래서 평균은 표준 자리(`duration`)에
113
+ * 두고, 변동은 이 확장 자리에 둔다 — 섞지 않는다.
114
+ *
115
+ * 표본은 **엔진의 난수원**으로 뽑는다(추정기가 자기 난수를 쓰면 fork 결정성이 깨진다).
116
+ */
117
+ export interface DurationVariability {
118
+ /** `constant`=변동 없음(기본) · `exponential`(평균=duration) · `uniform`·`triangular`(min/max 필수). */
119
+ distribution: 'constant' | 'exponential' | 'uniform' | 'triangular';
120
+ min?: IsoDuration;
121
+ max?: IsoDuration;
122
+ /** triangular 최빈값. 없으면 `duration` 을 최빈값으로 본다. */
123
+ mode?: IsoDuration;
124
+ }
125
+ /**
126
+ * 오퍼레이션 타입(공정 한 작업) — ISA-95 `OperationsSegment`.
127
+ *
128
+ * **시뮬레이션 명세가 여기 있어야 한다.** 예전에는 이 정의가 "무엇을·어디서·누가" 만 담고 "얼마나
129
+ * 걸리나·얼마나 성공하나" 는 커널 소스의 상수(30·20·40초, 수율 0.8)였다. 그래서 현장마다 다른 값을
130
+ * 데이터로 줄 방법이 없었고, 그 상수가 주목 신호(불량률 높음)까지 만들어 냈다.
131
+ */
132
+ export interface OperationDef {
133
+ key: string;
134
+ label: string;
135
+ intent: OperationIntent;
136
+ /** 오퍼레이션이 수행되는 자리 타입(LocationTypeDef.key). 커널이 locationByType 로 위치 해소. */
137
+ locationType?: string;
138
+ /** 요구 자원 종류(ResourceTypeDef.key). transport/process 는 자원 필요, dwell 은 무자원. */
139
+ resourceType?: string;
140
+ /** CBV bizStep URN(방출 이벤트 어휘). */
141
+ bizStep?: string;
142
+ /**
143
+ * 소요시간(평균/기준) — ISA-95 `OperationsSegment.Duration` 과 1:1. 없으면 커널 기본값을 쓰고,
144
+ * **기본값을 썼다는 사실을 숨기지 않는다**(`specCoverage()`).
145
+ */
146
+ duration?: IsoDuration;
147
+ /** 소요시간 변동(표준 밖 확장). 미지정 = 상수. */
148
+ variability?: DurationVariability;
149
+ /** 공정 모수(수율·셋업 등) — ISA-95 `ParameterSpecification` 1:1. ID 어휘는 `OP_PARAM`. */
150
+ parameters?: OperationParameter[];
151
+ /**
152
+ * 필요 인원 — **ISA-95 `OperationsSegment.PersonnelSpecification`**(`PersonnelClassID` + `Quantity`) 1:1.
153
+ *
154
+ * 이것이 없으면 사람은 시뮬레이션에 존재하지 않는다 — 설비만 있으면 언제나 가동되는 공장이 된다.
155
+ * 현장에서 가장 자주 부족한 자원이 사람인데, 그 부족이 만드는 줄이 예측에서 통째로 사라진다.
156
+ * 등급(class)으로 요구한다: 특정인을 지목하는 것이 아니라 "용접 자격자 2명" 이다.
157
+ */
158
+ personnelSpecification?: {
159
+ personnelClass?: string;
160
+ quantity: number;
161
+ }[];
162
+ /**
163
+ * 필요 물리 자산 — **ISA-95 `OperationsSegment.PhysicalAssetSpecification`** 과 같은 자리
164
+ * (`PhysicalAssetClassID` + `Quantity`). 빈 팔레트가 없어 출고가 못 나가는 일은 현장에서 잦다.
165
+ * 인원과 같은 규칙: 등급으로 요구하고, 모자라면 **부분 투입 없이 기다린다.**
166
+ */
167
+ physicalAssetSpecification?: {
168
+ assetClass?: string;
169
+ quantity: number;
170
+ }[];
171
+ /**
172
+ * 필요 설비 — **ISA-95 `OperationsSegment.EquipmentSpecification`**(`EquipmentClassID` + `Quantity`).
173
+ *
174
+ * 예전에는 작업 하나에 설비 **한 대**만 붙었다(`resourceType` 하나). 그래서 "크레인 1대 + 스프레더
175
+ * 1대", "용접 로봇 2대가 함께" 같은 현장을 표현할 수 없었고, 그 동시 점유가 만드는 줄이 예측에서
176
+ * 사라졌다. 인원·자산과 **같은 규칙**으로 요구한다: 등급 + 대수, 부분 확보 없이 전량 아니면 대기.
177
+ *
178
+ * 미지정이면 기존 거동(`resourceType` 한 대, 없으면 아무 유휴 설비).
179
+ */
180
+ equipmentSpecification?: {
181
+ equipmentClass?: string;
182
+ quantity: number;
183
+ }[];
184
+ /**
185
+ * 필요·산출 자재 — **ISA-95 `OperationsSegment.MaterialSpecification`**(`OpMaterialSpecificationType`).
186
+ *
187
+ * ── 4대 자원 중 자재만 빠져 있었다 ────────────────────────────────────────
188
+ * 인원·설비·자산 셋은 공정이 "등급 + 수량" 으로 요구하는데 **자재만 그 자리가 없었다.** 그래서
189
+ * 트레일러 조립 공장을 모델링해도 *"차축 하나에 바퀴 둘"* 을 말할 방법이 없고, 부품이 없어서
190
+ * 라인이 서는 상황이 **예측에 아예 나타나지 않는다** — 자재는 현장에서 사람만큼 자주 부족하다.
191
+ *
192
+ * ── BOM 은 어디 있나 (1차 출처 확인) ─────────────────────────────────────
193
+ * `MaterialDefinition.AssemblyDefinition` 은 **재귀 구조**(무엇이 무엇으로 이루어지나)일 뿐
194
+ * **수량이 없다.** 수량은 공정 쪽 `MaterialSpecification` 이 든다(`Quantity` + `MaterialUse`).
195
+ * 즉 표준에서 **BOM 의 "몇 개" 는 공정의 사실**이다 — 같은 부품이라도 공정마다 소요가 다르다.
196
+ *
197
+ * `use` 는 표준 `MaterialUse` 를 소문자로 쓴다(우리 어휘 규약). 지금 커널이 소비하는 것은
198
+ * `consumed`(작업이 시작되려면 있어야 하고 시작 시 빠진다)뿐이고, 나머지는 **선언만 받아 둔다** —
199
+ * 자리가 없으면 사실이 들어오지 못한다.
200
+ *
201
+ * ── 여기에 **품목별 BOM 을 넣지 않는다** (2026-08-22) ──────────────────────
202
+ * 이 자리는 ProcessSegment 쪽이다 — 트윈 전체에 한 벌이고 **어느 품목을 만드는 중인지 모른다**
203
+ * (`claimMaterials` 는 `operationSpecs.get(t.kind)` 로 찾는다). 그래서 여기 담기는 것은 **품목과
204
+ * 무관하게 그 자리가 늘 쓰는 것**이다: 포장 필름·세척수·윤활유 같은 `consumable`, 그 공정이 품목을
205
+ * 가리지 않고 먹는 부자재.
206
+ *
207
+ * 품목마다 다른 소요는 **레시피의 태그**가 든다(§`RecipePart.operation`). 실 데이터에서 이 둘을 섞으면
208
+ * 배합 로트 하나가 자재 631종 36톤을 먹는다 — 첫 실 연동이 그 크기를 재서 알려 주었다.
209
+ */
210
+ materialSpecification?: OpMaterialSpecification[];
211
+ }
212
+ /**
213
+ * 공정 하나의 자재 명세 — **ISA-95 `OpMaterialSpecificationType`** 의 우리 부분집합.
214
+ *
215
+ * 1차 출처: `ID` · `MaterialClassID*` · `MaterialDefinitionID*` · `MaterialLotID*` · `MaterialSubLotID*` ·
216
+ * `Description*` · **`MaterialUse`** · `HierarchyScope` · `StorageLocation` · `SpatialDefinition` ·
217
+ * **`Quantity*`** · `AssemblySpecification*`(재귀) · `AssemblyType` · `AssemblyRelationship` ·
218
+ * `MaterialSpecificationProperty*` · `TestSpecificationID*`.
219
+ *
220
+ * 우리는 **요구를 표현하는 데 필요한 것**만 든다(인원·자산 명세와 같은 모양). 로트·하위로트 지목,
221
+ * 재귀 조립 명세, 저장 위치 한정은 아직 없다 — 필요해질 때 표준 이름 그대로 얹는다.
222
+ */
223
+ export interface OpMaterialSpecification {
224
+ /** 표준 `ID` — 명세를 가리키는 이름(선택). */
225
+ id?: string;
226
+ /** 품목 등급으로 요구 — 표준 `MaterialClassID`. */
227
+ materialClass?: string;
228
+ /** 특정 품목으로 요구 — 표준 `MaterialDefinitionID`. 등급과 함께 주면 품목이 좁은 쪽이다. */
229
+ materialDefinition?: string;
230
+ /**
231
+ * 이 공정에서의 쓰임 — 표준 `MaterialUse`.
232
+ * `consumed` 없어지며 들어간다 · `produced` 만들어져 나온다 · `consumable` 쓰이지만 제품에 남지 않는다.
233
+ */
234
+ use: 'consumed' | 'produced' | 'consumable';
235
+ /** 수량 — 표준 `Quantity`. 단위 미지정이면 개수(EPCIS 규약과 같다). */
236
+ quantity: number;
237
+ uom?: string;
238
+ }
239
+ /** 라우트(오퍼레이션 시퀀스) — ISA-95 ProcessSegment 연결. */
240
+ export interface RouteDef {
241
+ key: string;
242
+ label: string;
243
+ /** OperationDef.key 순서. */
244
+ steps: string[];
245
+ /**
246
+ * **절차 계층**(ISA-88) — `steps` 위에 이름을 붙인다. 선언하지 않으면 지금과 똑같이 동작한다.
247
+ *
248
+ * ── 왜 트리를 따로 두나 (2026-08-30) ─────────────────────────────────────
249
+ * 배치 공정의 절차는 트리다: Procedure ⊃ UnitProcedure ⊃ Operation ⊃ Phase. 그런데 **실행되는 것은
250
+ * 잎뿐이다.** 그래서 `steps` 를 재귀 구조로 바꾸지 않는다 — 그러면 기존 선언이 전부 깨지고, 트리가
251
+ * 필요 없는 이산 제조까지 트리를 쓰게 된다.
252
+ *
253
+ * `steps` 가 실행 순서의 정본이고, 이것은 그 위에 얹는 이름이다.
254
+ *
255
+ * ── 왜 잎이 자기 단계를 드나 ───────────────────────────────────────────────
256
+ * 처음 안은 `stepParent: Record<string, string>` 을 따로 두는 것이었다. 그러면 트리가 **두 곳**에
257
+ * 있게 되어 `steps` 나 트리 어느 쪽이 바뀌어도 따로 고쳐야 하고, 한쪽만 고쳐지는 날 화면은 오류 없이
258
+ * 잘못된 트리를 보여 준다.
259
+ *
260
+ * 잎이 자기 `step` 을 들면 자리가 하나다. 그리고 「실행되는 것은 잎뿐」이 주석이 아니라 **모양으로**
261
+ * 막힌다 — 상위 요소에는 `step` 을 쓸 자리가 없다.
262
+ */
263
+ procedure?: ProcedureElementDef[];
264
+ }
265
+ /**
266
+ * 절차 트리의 **요소**(ISA-88 procedural element) — Procedure ⊃ UnitProcedure ⊃ Operation ⊃ Phase 의 한 자리.
267
+ *
268
+ * `UnitProcedure` 는 **한 설비 안에서 끝나는 묶음**이다. 커널에서는 같은 `locationType` 을 연속 단계로
269
+ * 선언하면 이동 없이 한 설비를 계속 쓰므로, 이 단은 **새 동작을 만드는 것이 아니라 이름을 붙이는 것**이다.
270
+ */
271
+ export type ProcedureLevel = 'Procedure' | 'UnitProcedure' | 'Operation' | 'Phase';
272
+ export interface ProcedureElementDef {
273
+ key: string;
274
+ label: string;
275
+ level: ProcedureLevel;
276
+ /** 상위 요소의 `key`. 없으면 최상위. */
277
+ parent?: string;
278
+ /**
279
+ * 이 노드가 실행하는 단계 — `RouteDef.steps` 의 키. **잎에만 있다.**
280
+ *
281
+ * 잎이 아닌 요소에 이것이 있으면 선언을 거부한다. 상위 요소는 실행 단위가 아니고, 커널이 그것을
282
+ * 공정으로 찾으면 `locationType` 이 없어 기동이 막힌다.
283
+ */
284
+ step?: string;
285
+ }
286
+ /**
287
+ * 절차 트리가 성립하는가 — **선언을 받아들이기 전에 본다.**
288
+ *
289
+ * 세 가지를 본다. 셋 다 어기면 화면이나 커널이 오류 없이 틀린 것을 보여 준다.
290
+ *
291
+ * 상위 노드가 단계를 든다 실행 단위가 아닌 것을 실행하려 든다
292
+ * 한 단계가 잎 여럿에 실린다 그 단계가 절차 어디에 있는지 말할 수 없다
293
+ * 모르는 단계·모르는 부모 가리키는 것이 없다
294
+ */
295
+ export declare function procedureViolations(route: RouteDef): string[];
296
+ export interface RecipePart {
297
+ /** MaterialDef.key. */
298
+ material: string;
299
+ qty: number;
300
+ /**
301
+ * **이 투입이 들어가는 공정** — `RouteDef.steps` 의 키. 없으면 오더 착수에 확보한다.
302
+ *
303
+ * ── 왜 여기인가 (2026-08-22) ──────────────────────────────────────────────
304
+ * ISA-95 에는 자재 명세가 붙는 자리가 **둘**이고 뜻이 다르다.
305
+ *
306
+ * · `ProcessSegment` — 그 자리가 **할 수 있는 일**(능력·소요시간·필요 자원). 품목에 매이지 않는다.
307
+ * · `OperationsSegment` — 그 **품목을 만드는 절차**의 한 단계(`OperationsDefinition` 소속). 품목에 매인다.
308
+ *
309
+ * `OperationDef` 는 ProcessSegment 쪽이다 — 트윈 전체에 한 벌이고 `locationType`·소요시간·설비 종류를
310
+ * 든다. 거기에 품목별 BOM 을 붙이면 **범주 오류**다. 실 데이터가 그 크기를 보였다: 「배합」에 붙이면
311
+ * 배합을 지나는 **모든** 로트가 자재 631종 36톤을 먹는다(실제로는 그 로트 한 품목분 수 kg).
312
+ *
313
+ * 품목별 공정 소요가 사는 자리는 OperationsSegment 이고, 이 커널에서 그 범위를 가진 것은 **레시피**다.
314
+ * 그래서 태그를 레시피 투입에 둔다.
315
+ *
316
+ * **`RecipeDef.steps[]` 로 두지 않는 이유**: 공정의 순서는 `RouteDef.steps` 가 이미 소유한다. 레시피에
317
+ * 단계 목록을 또 두면 두 목록이 어긋날 수 있고, 어긋났을 때 어느 쪽이 맞는지 말할 근거가 없다.
318
+ * 태그 하나면 **순서는 라우트가, 소요는 (레시피 × 공정)이** 말한다 — 한 사실에 한 자리다.
319
+ *
320
+ * 첫 실 연동(F&B MES)의 BOM 이 (만드는 품목, 공정) 단위이고, 경로가 둘 이상인 레시피가 306/471 이다.
321
+ */
322
+ operation?: string;
323
+ }
324
+ /** BOM/레시피 — ISA-95 Material Consumed/Produced · EPCIS TransformationEvent(input→output). */
325
+ export interface RecipeDef {
326
+ key: string;
327
+ label: string;
328
+ inputs: RecipePart[];
329
+ outputs: RecipePart[];
330
+ /** RouteDef.key(선택) — 이 산출물을 만드는 공정 경로. */
331
+ route?: string;
332
+ }
333
+ /** CBV 어휘(bizStep·거래유형). */
334
+ export interface DomainVocabulary {
335
+ bizSteps?: Record<string, string>;
336
+ transactionTypes?: Record<string, string>;
337
+ }
338
+ /**
339
+ * 도메인 정의 — 커널이 소비하는 실행 계약. (스토어 메타 version/supplier 는 catalog 래퍼가 얹음.)
340
+ */
341
+ export interface DomainDefinition {
342
+ id: string;
343
+ label: string;
344
+ vocabulary?: DomainVocabulary;
345
+ materials?: MaterialDef[];
346
+ locationTypes: LocationTypeDef[];
347
+ resourceTypes: ResourceTypeDef[];
348
+ operations?: OperationDef[];
349
+ routes?: RouteDef[];
350
+ recipes?: RecipeDef[];
351
+ }
352
+ /**
353
+ * 구조·참조무결성 검증(zero-dep, `validateEpcisEvent` 스타일). 위반 목록 반환(빈 배열 = 유효).
354
+ * 도메인 정의는 데이터 아티팩트라 로드 시점에 런타임 검증한다(컴파일타임 아님).
355
+ */
356
+ export declare function validateDomainDefinition(def: DomainDefinition): string[];
@@ -0,0 +1,137 @@
1
+ /*
2
+ * 도메인 정의(DomainDefinition) — 커널의 선언적 도메인 계약. `TwinModelDef`(토폴로지 입력)의 확장 격.
3
+ * 커널이 "무엇이 있고 어떻게 흐르나"(타입 + 공정 route/BOM)를 **데이터로** 받는 형식. zero-dep(자기 타입).
4
+ *
5
+ * 특정 공정이 커널 코드에 하드코딩되던 것을 이 데이터 계약으로 대체한다(design/plans/domain-catalog-layering.md).
6
+ * 스토어 패키지 `@operato/twin-catalog` 가 이 타입을 import 해 스토어 메타(version/supplier)를 얹어 배포한다(catalog → kernel 의존).
7
+ * 표준 앵커: GS1 EPCIS 2.0(bizStep) · ISA-95(WorkCenter·OperationsDefinition·BOM) · ISO 55000(Asset).
8
+ */
9
+ /**
10
+ * 우리가 소비하는 canonical 파라미터 ID — **표준이 이름을 정해 주지 않는 자리**이므로 여기서 한 번 정한다.
11
+ * 새 모수를 쓸 때는 반드시 여기에 등록한다(문자열 리터럴을 코드에 흩뿌리지 않는다).
12
+ */
13
+ export const OP_PARAM = {
14
+ /** 양품률(0..1, 무차원). 없으면 커널 기본값 — 기본값을 쓴 사실은 `specCoverage()` 가 밝힌다. */
15
+ yield: 'yield',
16
+ /** 셋업·체인지오버 소요(ISO 8601 기간 문자열). ISA-95 는 셋업을 별도 세그먼트로도 표현하지만,
17
+ * 현재 커널은 작업에 붙는 셋업으로 다루므로 모수로 받는다. */
18
+ setupDuration: 'setupDuration'
19
+ };
20
+ /**
21
+ * 절차 트리가 성립하는가 — **선언을 받아들이기 전에 본다.**
22
+ *
23
+ * 세 가지를 본다. 셋 다 어기면 화면이나 커널이 오류 없이 틀린 것을 보여 준다.
24
+ *
25
+ * 상위 노드가 단계를 든다 실행 단위가 아닌 것을 실행하려 든다
26
+ * 한 단계가 잎 여럿에 실린다 그 단계가 절차 어디에 있는지 말할 수 없다
27
+ * 모르는 단계·모르는 부모 가리키는 것이 없다
28
+ */
29
+ export function procedureViolations(route) {
30
+ const elements = route.procedure ?? [];
31
+ if (!elements.length)
32
+ return [];
33
+ const errors = [];
34
+ const byKey = new Map(elements.map(e => [e.key, e]));
35
+ const hasChild = new Set(elements.map(e => e.parent).filter((k) => !!k));
36
+ const stepSet = new Set(route.steps ?? []);
37
+ const seenStep = new Map();
38
+ for (const n of elements) {
39
+ if (n.parent && !byKey.has(n.parent))
40
+ errors.push(`${n.key}: 상위 '${n.parent}' 가 이 절차에 없다`);
41
+ if (n.step === undefined)
42
+ continue;
43
+ if (hasChild.has(n.key))
44
+ errors.push(`${n.key}: 아래 요소가 있는데 단계를 든다 — 실행되는 것은 잎뿐이다`);
45
+ if (!stepSet.has(n.step))
46
+ errors.push(`${n.key}: 모르는 단계 '${n.step}' — steps 에 없다`);
47
+ const already = seenStep.get(n.step);
48
+ if (already)
49
+ errors.push(`단계 '${n.step}' 가 '${already}' 와 '${n.key}' 둘에 실렸다 — 절차 어디인지 말할 수 없다`);
50
+ else
51
+ seenStep.set(n.step, n.key);
52
+ }
53
+ return errors;
54
+ }
55
+ const INTENTS = ['transport', 'process', 'dwell'];
56
+ function dupes(keys) {
57
+ const seen = new Set();
58
+ const dup = new Set();
59
+ for (const k of keys) {
60
+ if (seen.has(k))
61
+ dup.add(k);
62
+ else
63
+ seen.add(k);
64
+ }
65
+ return [...dup];
66
+ }
67
+ /**
68
+ * 구조·참조무결성 검증(zero-dep, `validateEpcisEvent` 스타일). 위반 목록 반환(빈 배열 = 유효).
69
+ * 도메인 정의는 데이터 아티팩트라 로드 시점에 런타임 검증한다(컴파일타임 아님).
70
+ */
71
+ export function validateDomainDefinition(def) {
72
+ const v = [];
73
+ if (!def || typeof def !== 'object')
74
+ return ['domain definition 없음/객체 아님'];
75
+ if (typeof def.id !== 'string' || !def.id)
76
+ v.push('id 누락');
77
+ if (typeof def.label !== 'string' || !def.label)
78
+ v.push('label 누락');
79
+ if (!Array.isArray(def.locationTypes) || def.locationTypes.length === 0)
80
+ v.push('locationTypes 비어있음');
81
+ if (!Array.isArray(def.resourceTypes))
82
+ v.push('resourceTypes 배열 아님');
83
+ const locationKeys = new Set((def.locationTypes || []).map(n => n.key));
84
+ const resKeys = new Set((def.resourceTypes || []).map(r => r.key));
85
+ const matKeys = new Set((def.materials || []).map(m => m.key));
86
+ const matByKey = new Map((def.materials || []).map(m => [m.key, m]));
87
+ const opKeys = new Set((def.operations || []).map(o => o.key));
88
+ const routeKeys = new Set((def.routes || []).map(r => r.key));
89
+ for (const [name, arr] of [['locationTypes', def.locationTypes], ['resourceTypes', def.resourceTypes], ['materials', def.materials], ['operations', def.operations], ['routes', def.routes], ['recipes', def.recipes]]) {
90
+ for (const d of dupes((arr || []).map(x => x.key)))
91
+ v.push(`${name} 키 중복: ${d}`);
92
+ }
93
+ for (const o of def.operations || []) {
94
+ if (!INTENTS.includes(o.intent))
95
+ v.push(`operation '${o.key}' intent 부정: ${o.intent}`);
96
+ if (o.locationType && !locationKeys.has(o.locationType))
97
+ v.push(`operation '${o.key}' locationType '${o.locationType}' 미정의`);
98
+ if (o.resourceType && !resKeys.has(o.resourceType))
99
+ v.push(`operation '${o.key}' resourceType '${o.resourceType}' 미정의`);
100
+ if (o.intent === 'dwell' && o.resourceType)
101
+ v.push(`operation '${o.key}' dwell 인데 resourceType 지정됨(무자원이어야)`);
102
+ }
103
+ for (const r of def.routes || []) {
104
+ if (!Array.isArray(r.steps) || r.steps.length === 0)
105
+ v.push(`route '${r.key}' steps 비어있음`);
106
+ for (const s of r.steps || [])
107
+ if (!opKeys.has(s))
108
+ v.push(`route '${r.key}' step '${s}' 미정의 operation`);
109
+ }
110
+ for (const rc of def.recipes || []) {
111
+ if (rc.route && !routeKeys.has(rc.route))
112
+ v.push(`recipe '${rc.key}' route '${rc.route}' 미정의`);
113
+ if (!rc.outputs?.length)
114
+ v.push(`recipe '${rc.key}' outputs 비어있음`);
115
+ for (const p of [...(rc.inputs || []), ...(rc.outputs || [])]) {
116
+ if (!matKeys.has(p.material))
117
+ v.push(`recipe '${rc.key}' material '${p.material}' 미정의`);
118
+ if (typeof p.qty !== 'number' || p.qty <= 0)
119
+ v.push(`recipe '${rc.key}' material '${p.material}' qty 부정`);
120
+ /* 레시피가 쓰는 자재는 자기 자리를 말해야 한다 — 커널은 그것을 지어내지 않는다(MaterialDef.locationType). */
121
+ const mat = matByKey.get(p.material);
122
+ /*
123
+ * ── 보관처는 **요구가 아니다** (2026-08-22) ────────────────────────────────
124
+ * 예전에는 레시피가 쓰는 자재에 `locationType` 을 요구했다. 그런데 **재고로 위치를 말하는
125
+ * 시스템**에는 그 선언이 없다 — 자재에 고정된 보관처를 두지 않는 것이 WMS 계열의 정상이고,
126
+ * 표준도 위치를 정의(`MaterialDefinition`)가 아니라 로트(`MaterialLot`)에 둔다.
127
+ *
128
+ * 실측이 그 비용을 보였다: 첫 실 연동에서 원자재 986건 중 보관처가 선언된 것이 36건이었고, 그
129
+ * 요구 때문에 레시피 **937/1,408 건이 아예 실리지 못했다.** 확보는 품목 색인으로 돌므로
130
+ * (§`ItemStore.ofGtin`) 선언은 있으면 좁히는 **정책**이고 없어도 정확하다.
131
+ */
132
+ if (mat?.locationType && !locationKeys.has(mat.locationType))
133
+ v.push(`recipe '${rc.key}' material '${p.material}' locationType '${mat.locationType}' 미정의`);
134
+ }
135
+ }
136
+ return v;
137
+ }
@@ -0,0 +1,147 @@
1
+ import type { TwinTypeInfo } from './domain-catalog.ts';
2
+ /** 로케이션(수동) 타입 키 — 배전 계통의 구간. */
3
+ export declare const EMS_LOCATION_TYPES: readonly ["incoming", "feeder", "submeter-zone"];
4
+ /**
5
+ * 계량 지점의 방향 — **무엇을 재는 계량기인가.**
6
+ *
7
+ * import 쓰는 쪽. 계통에서 받는다(대부분의 계량기).
8
+ * export 내는 쪽. 발전이 계통으로 나가는 것을 잰다.
9
+ * bidirectional 양쪽을 한 계량기로 잰다(상계 거래·축전지). 부호가 방향을 담는다.
10
+ *
11
+ * 선언하지 않으면 **모르는 것**이고, 모르는 것은 부하로 센다. 빼면 부하가 오류 없이 작아지고, 작아진
12
+ * 부하는 「계약 안쪽」이라는 더 위험한 거짓을 만든다(같은 이유로 정체 모를 계량기도 합에 넣는다).
13
+ *
14
+ * `bidirectional` 은 **그 계량기가 순 유입을 재는 것**이다. 발전을 부하에서 빼는 것이 아니다 — 그
15
+ * 계량기가 읽은 값이 그 접속점의 사실이고, 요금이 그 값에 매겨진다.
16
+ *
17
+ * 표준 근거: 계량기의 적산 레지스터가 유입·유출로 갈려 있고(IEC 62053 계열), ISO 50001 은 유입 경계를
18
+ * 따로 둔다(`EnergyInput`). 우리가 지은 낱말이 아니다.
19
+ */
20
+ export declare const METER_DIRECTION: readonly ["import", "export", "bidirectional"];
21
+ export type MeterDirection = (typeof METER_DIRECTION)[number];
22
+ /** 설비(능동) 타입 키 — 계량 지점과 에너지 자원. */
23
+ export declare const EMS_EQUIPMENT_TYPES: readonly ["meter", "breaker", "pv-array", "battery", "utility", "curtailable-load"];
24
+ /**
25
+ * 커널이 **읽는** 자리 속성 — 뜻을 코드 한가운데 숨기지 않는다.
26
+ *
27
+ * 속성 자체는 열려 있고(`ResourceProperty`) 어휘는 표준이 정하지 않는다. 그래서 커널이 판정에 쓰는
28
+ * 것만 여기 이름으로 못 박는다 — 이 목록에 없는 속성은 커널이 나르기만 하고 해석하지 않는다.
29
+ */
30
+ export declare const EMS_PROPERTY: {
31
+ /** 계약전력(kW) — 수전·분기 자리에 선언한다. 없으면 계약 대비 판정을 하지 않는다. */
32
+ readonly contractKW: "contract.kW";
33
+ readonly meterDirection: "meter.direction";
34
+ /** 가동 중 소비(kW). */
35
+ readonly ratedKW: "power.ratedKW";
36
+ /** 멈춰 있을 때의 소비(kW). 없으면 멈춘 동안을 **비운다** — 0 이라고 주장하지 않는다. */
37
+ readonly standbyKW: "power.standbyKW";
38
+ /** 기본요금 단가 — 최대수요 1kW 당(청구 주기 기준). */
39
+ readonly demandChargePerKW: "tariff.demandChargePerKW";
40
+ /**
41
+ * **요금적용전력**(kW) — 기본요금이 실제로 매겨지는 기준. 계약전력과 다른 값이다.
42
+ *
43
+ * 기본요금이 걸리는 수는 이 주기에 잰 최대가 아닌 경우가 많다 — 지난 몇 달의 최고를 끌고 가거나,
44
+ * 약정 용량으로 매기거나, 사업자가 따로 정한다. 그 규칙은 나라와 계약마다 다르므로 커널이 계산하지
45
+ * 않고 **계산된 결과를 받는다.** 이 값이 있으면 기본요금이 조건부 파생이 아니라 사실이 된다.
46
+ *
47
+ * 계약전력(`contract.kW`)을 이 자리에 넣지 말 것. 넘었을 때의 뜻이 다르다 — 계약 초과는 약정
48
+ * 위반이고, 이쪽은 다음 주기의 기본요금이 오른다는 뜻이다.
49
+ */
50
+ readonly billingDemandKW: "tariff.billingDemandKW";
51
+ /** 사용량 단가 — 1kWh 당. */
52
+ readonly energyChargePerKWh: "tariff.energyChargePerKWh";
53
+ /** 통화 — ISO 4217 코드(USD·KRW…). 없으면 금액에 단위를 붙이지 않는다. */
54
+ readonly currency: "tariff.currency";
55
+ /** 축전지 용량(kWh) — SOC 를 에너지로 바꾸는 값. 없으면 방전을 만들지 않는다. */
56
+ readonly capacityKWh: "storage.capacityKWh";
57
+ /** 이 순수요를 넘으면 방전한다(kW) — 피크 억제 임계. */
58
+ readonly dischargeAboveKW: "dispatch.dischargeAboveKW";
59
+ /** 최대 방전율(kW) — 인버터가 낼 수 있는 한계. */
60
+ readonly maxDischargeKW: "dispatch.maxDischargeKW";
61
+ /** 예비 SOC(%) — 이 아래로는 쓰지 않는다(비상 대비). 없으면 0 으로 본다. */
62
+ readonly reserveSoc: "dispatch.reserveSoc";
63
+ /**
64
+ * 시뮬레이션이 **출발할 때의 SOC(%)** — 씨앗값이다.
65
+ *
66
+ * 계측이 SOC 를 알려 주는 트윈에는 필요 없다(잰 값이 진실이다). 시뮬 트윈에는 알려 줄 것이 없어서
67
+ * 「SOC 를 모르면 방전하지 않는다」는 규율에 걸려 **배터리가 아무 일도 하지 못했다** — 정책을 선언해도
68
+ * 피크가 한 톨도 깎이지 않았다(실화면에서 그렇게 났다).
69
+ *
70
+ * 이것은 상태의 씨앗이지 계측이 아니다. 그래서 계측이 들어오는 순간 그것이 이긴다.
71
+ */
72
+ readonly initialSoc: "storage.initialSoc";
73
+ /**
74
+ * 발전 정격(kW) — **교류 쪽 최대 출력**. 계통으로 나가는 값이다.
75
+ *
76
+ * 이용률의 분모가 이 값이다(만든 양 ÷ 정격 × 시간). **직류 쪽 정격(§`genRatedKWdc`)과 다르고 더 작다** —
77
+ * 두 값을 한 이름에 담으면 이용률이 그 차이만큼 틀리고, 틀린 이유가 아무 데도 표시되지 않는다.
78
+ *
79
+ * 설비마다 선언하거나, 설비별 값을 모르는 현장은 **자리(수전)에 합계로** 선언한다.
80
+ */
81
+ readonly genRatedKW: "generation.ratedKW";
82
+ /**
83
+ * 발전 정격(kW) — **직류 쪽**. 변환기(인버터)의 입력 쪽 정격이고, 태양광이면 패널 정격의 합이다.
84
+ *
85
+ * 이용률의 분모로 쓰지 않는다. 이 값이 교류 정격보다 큰 것은 설계이고(변환기를 그렇게 고른다),
86
+ * 그 차이가 「맑은 정오에도 교류 출력이 더 오르지 않는 이유」를 설명한다.
87
+ */
88
+ readonly genRatedKWdc: "generation.ratedKWdc";
89
+ /**
90
+ * 하루 형상 — 쉼표로 나눈 **24개 비율**(0~1). 시각(현장 시간대)의 정격 대비 출력이다.
91
+ *
92
+ * 예: `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`
93
+ */
94
+ readonly genDailyProfile: "generation.dailyProfile";
95
+ };
96
+ /**
97
+ * 이 선언들의 **단위와 범위** — 값을 읽는 쪽이 아니라 **주는 쪽**을 위한 표다.
98
+ *
99
+ * ── 왜 커널이 단위를 말해야 하나 (2026-08-17) ────────────────────────────────
100
+ * 사용자가 「현재 SOC 80%」라고 말했고, 저작 AI 는 `storage.initialSoc = 0.8` 을 썼다. 커널은 이 값을
101
+ * **퍼센트**로 읽으므로(`(soc - reserve) / 100`) 그 배터리는 0.8% 로 선언됐다 — 쓸 수 있는 에너지가
102
+ * 사실상 없는 상태이고, **경고 하나 없이** 그렇게 됐다. 실화면에서 그렇게 났다.
103
+ *
104
+ * 잘못은 값을 준 쪽이 아니라 **단위를 말하지 않은 쪽**에 있다. 타입을 지어내지 못하게 카탈로그를 주면서
105
+ * 속성은 id 만 줬으니, 값을 만드는 쪽은 단위를 짐작할 수밖에 없었다.
106
+ *
107
+ * 표를 커널에 두는 이유: 이 단위는 **커널의 셈법이 정하는 것**이다(퍼센트로 나누는 코드가 여기 있다).
108
+ * 소비처마다 적으면 두 벌이 되고, 두 벌은 갈라진다 — 갈라지는 순간 이 결함이 그대로 돌아온다.
109
+ */
110
+ export interface EmsPropertySpec {
111
+ /** 단위 — 사람이 읽는 표기(kW·kWh·% 등). 무차원이면 없다. */
112
+ uom?: string;
113
+ /** 값의 종류 — GS1 dataType 표기(`xs:double`·`xs:string`). */
114
+ dataType: 'xs:double' | 'xs:string';
115
+ /** 허용 범위 [min, max] — 밖의 값은 받는 쪽이 거절할 수 있다(있는 것만 적는다). */
116
+ range?: [number, number];
117
+ /** 무엇을 뜻하나 — 영어 canonical(표현 계층이 사용자 언어로 옮긴다). */
118
+ note: string;
119
+ }
120
+ export declare const EMS_PROPERTY_SPEC: Record<string, EmsPropertySpec>;
121
+ /**
122
+ * 하루 형상에서 **이 시각의 비율**을 읽는다 — 없거나 형태가 아니면 `undefined`(발전하지 않는다).
123
+ *
124
+ * 24개가 아니면 받지 않는다: 값이 몇 개인지 짐작해 늘리거나 자르면, 사람이 적은 곡선과 우리가 쓰는 곡선이
125
+ * 달라진다. 0~1 밖의 값도 받지 않는다(정격의 배수로 발전하는 태양광은 없다).
126
+ */
127
+ export declare function generationFractionAt(profile: string | undefined | null, hourOfDay: number): number | undefined;
128
+ export declare const EMS_TYPES: TwinTypeInfo[];
129
+ /**
130
+ * 이 자리의 **전기 상류** — 어디서 전기를 받는가.
131
+ *
132
+ * ── 왜 함수로 두나 (2026-08-18) ─────────────────────────────────────────────
133
+ * 두 세대가 섞여 있다. 새 모델은 `upstream` 을 적고, 그 전 모델은 분기의 `parentId` 에 수전을 적었다.
134
+ * 판정을 소비처마다 쓰면(호스트·계통도·커널) 한쪽만 고쳐지고 그때부터 화면과 계산이 다른 계통을 말한다.
135
+ * 그래서 **읽는 규칙을 한 곳**에 둔다.
136
+ *
137
+ * 옛 세대를 흡수하는 조건이 좁다: `parentId` 가 **전기 자리**(수전·분기·구역 계량)를 가리킬 때만
138
+ * 상류로 읽는다. 공장·구역 같은 공간 부모를 상류로 읽으면 없는 결선을 만들어 낸다.
139
+ */
140
+ export declare function electricalUpstreamOf(locations: readonly {
141
+ id: string;
142
+ type?: string;
143
+ parentId?: string;
144
+ upstreamId?: string;
145
+ }[] | undefined | null, id: string): string | undefined;
146
+ /** 전기 계통의 자리인가 — 수전·분기·구역 계량. */
147
+ export declare function isElectricalLocationType(type: string): boolean;