@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,3557 @@
1
+ export type ISOTime = string;
2
+ export interface CanonicalEnvelope<T = unknown> {
3
+ eventId: string;
4
+ eventType: string;
5
+ eventTime: ISOTime;
6
+ tenantId: string;
7
+ correlationId?: string;
8
+ /**
9
+ * **사람이 그때 적어 둔 말** — 연결된 시스템에서 온 자유 문장.
10
+ *
11
+ * ── 왜 이 자리가 필요한가 (2026-08-25) ────────────────────────────────────
12
+ * 재고 조정만은 **원인이 트윈의 모델 밖에 있다.** 다른 이동은 원인을 트윈이 스스로 압니다 —
13
+ * 꺼내 간 것은 작업 지시가 시켰고, 적치는 입고가 시켰습니다. 그런데 조정은 **사람이 판단한 것**이고,
14
+ * 그 이유는 그 사람이 적은 문장에만 있습니다(「실재고로 조정(192kg→198kg)」).
15
+ *
16
+ * 그 문장이 없으면 트윈은 「6kg 이 늘었다」까지만 알고 **왜인지 영원히 답하지 못합니다.**
17
+ *
18
+ * ── 왜 봉투에 두고 상태에 두지 않나 ──────────────────────────────────────
19
+ * 그 문장은 **지난 사건에 대한 사실**이고 지금 수량에 대한 사실이 아닙니다. 상태에 두면 조정 열 번에
20
+ * 문장 열 개가 쌓여 시간에 비례해 자랍니다. 지난 기록이 답하는 물음입니다.
21
+ *
22
+ * 그리고 EPCIS 사건 안에 넣지 않습니다 — 표준에 그 칸이 없고, 이름을 지어 넣으면 그 사건이
23
+ * 표준을 벗어납니다. 봉투는 우리 것이므로 여기가 맞고, 지난 기록에는 봉투가 그대로 남습니다.
24
+ *
25
+ * ── 커널은 읽지 않습니다 ─────────────────────────────────────────────────
26
+ * 자유 문장이므로 어떤 판단에도 쓰지 않습니다. 나르기만 합니다. 비어 있으면 이 칸을 만들지
27
+ * 않습니다 — 빈 문장은 「적지 않았다」와 다르게 보입니다.
28
+ *
29
+ * 표준 근거: ISA-95 가 거의 모든 타입에 두는 `Description`(DescriptionType). 사람의 글을 담는
30
+ * 표준 자리이고, EPCIS 핵심에는 없어 확장으로 갑니다.
31
+ */
32
+ description?: string;
33
+ data: T;
34
+ }
35
+ /**
36
+ * 작업의 상태 — **ISA-95 근거.** 1차 출처: `JobOrderType.DispatchStatus`(지시 쪽) +
37
+ * `JobResponseType.JobState`(실적 쪽). 대조 기록: `design/plans/isa95-coverage.md` §3-2
38
+ * (「`tasks.status` ↔ `DispatchStatus`+`JobState`」).
39
+ *
40
+ * ── 왜 우리 이름을 쓰나 ─────────────────────────────────────────────────────
41
+ * 표준은 **지시(`JobOrder`)와 실적(`JobResponse`)을 나눈다.** 우리 `TaskState` 는 그 둘을 한 몸에
42
+ * 든다 — 상태 전이와 배정 결과를 함께 들고 진행·완료까지 같은 개체가 표현한다. 그래서 `jobOrders`
43
+ * 로 부르면 **표준 이름을 쓰면서 다른 뜻으로 쓰는 것**이 된다. 대조 문서가 그 판정을 적어 두었다.
44
+ *
45
+ * ── 왜 **닫혀** 있나 ────────────────────────────────────────────────────────
46
+ * 커널이 이 넷으로 처리한다: 성과 폴드가 착수·완료로 갈리고(`foldTaskRecords`), 주목 계산이
47
+ * `created`·`assigned` 를 대기로 센다, 배정 루프는 `created` 만 집는다. 그러니 낱말이 하나 늘면
48
+ * 그 셋이 조용히 달라진다 — 유입 문이 이 넷만 받는 이유다(다른 낱말은 오류 없이 사라진다).
49
+ */
50
+ export type TaskStatus = 'created' | 'assigned' | 'in-progress' | 'completed';
51
+ /** 오더가 **종결 상태**로 인정되는 낱말 — 이 밖은 진행 중이다(§`isOrderTerminal`). */
52
+ export declare const ORDER_TERMINAL_STATUS: readonly ["completed", "cancelled"];
53
+ /**
54
+ * 이 오더는 **끝났나** — 판정을 한 곳에 둔다(`locationStatusOf`·`dueStatusOf` 와 같은 규율).
55
+ *
56
+ * ── 왜 함수여야 하나 (2026-08-23) ───────────────────────────────────────────
57
+ * 이 판정이 없던 동안 호스트의 성과 폴드가 **낱말을 동의어 목록으로 추론했다**
58
+ * (`completed`·`fulfilled`·`done`·`finished`). 그 방식이 낸 대가가 둘이다.
59
+ *
60
+ * ① **원본의 낱말이 목록에 없으면 완료가 조용히 0 이 된다.** 실제로 그랬다 — 첫 실 시스템의
61
+ * `FINISHED` 가 빠져 있었고, 사람이 화면을 보고 의심해서야 드러났다. 그리고 그때 목록에 낱말을
62
+ * 더한 것은 진짜 빈 자리(커넥터가 자기 상태 표를 안 쓰고 있었다)를 가린 땜빵이었다.
63
+ * ② **시뮬 트윈에서는 언제나 0 이었다.** 시뮬은 오더를 이행하면서 `fulfilled` 만 올리고 `status` 는
64
+ * 바꾸지 않는다(그런 대입이 코드에 없다). 그래서 낱말로 묻는 판정은 시뮬의 완료를 **한 건도**
65
+ * 세지 못했다. 낱말이 틀린 것이 아니라 **묻는 축이 틀렸다.**
66
+ *
67
+ * ── 표준이 그 축을 이미 갈라 두었다 ─────────────────────────────────────────
68
+ * `orders` 는 `OperationsRequest` 쪽이고 그 상태는 `RequestState` 다 — **표준도 그 값을 열거하지
69
+ * 않는다.** 그래서 이 축이 열려 있는 것은 방언이 아니라 표준 정합이다. 대신 표준은 「됐나」를 **실적**
70
+ * 에서 읽는다(`JobResponse`·`SegmentResponse`, 그리고 요구는 `SegmentRequirement.Quantity`).
71
+ *
72
+ * 그러므로 이 판정의 1차 근거는 **양**이다: 요구한 만큼 이행됐으면 끝난 것이고, 그 사실은 어느 원본의
73
+ * 어느 낱말과도 무관하다. 낱말은 **양으로 말할 수 없는 종결**을 위해 함께 본다 — 취소, 그리고 요구량을
74
+ * 채우지 못한 채 닫힌 오더.
75
+ *
76
+ * ── 도메인이 상태에 다른 것을 적는다 ────────────────────────────────────────
77
+ * MES 커널은 이 자리에 **진행 단계**를 적는다(`op-<공정키>`, `blocked-seed-incomplete`). 그 값들은
78
+ * 종결이 아니므로 이 판정이 옳게 「아니다」로 답한다. 다만 진행 단계를 상태 문자열에 적는 것 자체가
79
+ * 표준의 모양은 아니다(그것은 `SegmentResponse` 의 일이다) — 그 빚은 ADR-0039 에 조건과 함께 적었다.
80
+ *
81
+ * 모르면 `false` 다 — 「끝나지 않았다」가 아니라 **「끝났다고 말할 근거가 없다」**이고, 성과는 근거가
82
+ * 있는 것만 센다(없는 완료를 세면 처리량이 조용히 부풀려진다).
83
+ */
84
+ export declare function isOrderTerminal(o: {
85
+ status?: string;
86
+ requested?: number;
87
+ fulfilled?: number;
88
+ }): boolean;
89
+ /**
90
+ * 자리의 상태 — **포화도에서 파생한다.** 저장하는 값이 아니다.
91
+ *
92
+ * 예전에는 시뮬이 `'idle'` 로 두고 한 번도 바꾸지 않았고(변경 지점 0), 미러에는 자리 상태 채널이
93
+ * 없어 비어 있었다. 화면은 그 값을 그대로 보여 주고 있었다 — **정보처럼 보이는데 정보가 아니었다.**
94
+ *
95
+ * 문턱은 병목 주목(`deriveAttentions`)이 쓰는 것과 **같다**: 90% 이상이면 임박, 100% 이상이면 포화.
96
+ * 규칙이 둘이면 화면과 주목이 다른 말을 한다. 용량을 모르면 상태도 모른다(undefined — 꾸미지 않는다).
97
+ */
98
+ export declare const LOCATION_SATURATION_NEAR = 0.9;
99
+ export declare function locationStatusOf(n: {
100
+ occupancy?: number;
101
+ capacity?: number;
102
+ }): 'available' | 'near-full' | 'full' | undefined;
103
+ /**
104
+ * 설비 계층 단계 — **ISA-95 표준 어휘.** 1차 출처: B2MML `B2MML-Common.xsd` /
105
+ * `EquipmentLevel1Type` 열거값 + `EquipmentLevelType` 주석("role based equipment hierarchy level
106
+ * as defined in ISA 95").
107
+ *
108
+ * 우리가 단의 이름을 발명하지 않는다. "라인" 은 `ProductionLine`, "존" 은 `StorageZone` 으로
109
+ * 표준이 이미 정해 뒀다. 발명하면 그 순간 방언이 되고, 연동 상대와 매핑 표가 필요해진다.
110
+ *
111
+ * **`Other` 는 탈출구다** — 표준도 열거값 밖을 인정한다(`OtherValue` 속성). 억지로 끼워 맞추는 대신
112
+ * `Other` 로 두고 현장의 낱말은 `type` 에 남긴다.
113
+ *
114
+ * `StorageZone`·`StorageUnit` 도 이 계층 안에 있다. 다만 **자리 자체는 다른 축**이다 —
115
+ * ISA-95 는 `OperationalLocation`("자원이 놓이거나 놓일 것으로 예상되는 논리적·물리적 장소",
116
+ * `B2MML-OperationalLocation.xsd`)을 별도 스키마로 두고, `Equipment` 가 자기 위치를 그것으로 가리킨다.
117
+ * 우리 `locations` 가 그 개념이다(2026-08-01 개명 — `plans/isa95-coverage.md` §3-1).
118
+ */
119
+ export declare const EQUIPMENT_LEVEL: readonly ["Enterprise", "Site", "Area", "ProcessCell", "Unit", "ProductionLine", "WorkCell", "ProductionUnit", "StorageZone", "StorageUnit", "WorkCenter", "WorkUnit", "EquipmentModule", "ControlModule", "Other"];
120
+ export type EquipmentLevel = (typeof EQUIPMENT_LEVEL)[number];
121
+ /** 표준 열거값인지 — 상류에서 들어온 값을 조용히 통과시키지 않고 확인하는 용도. */
122
+ export declare function isEquipmentLevel(v: unknown): v is EquipmentLevel;
123
+ /**
124
+ * 계층 질의 — **사슬을 걷는 규칙 한 벌.**
125
+ *
126
+ * 왜 커널이 내는가: 자리는 자리 밑에 들 수 있고(라인 ⊃ 스테이션), 설비는 자리에 붙박인다. 그래서
127
+ * "이 라인의 처리량" 같은 질문은 사슬을 걷어야 답이 나온다. 소비처(화면·집계·AI)가 각자 걷게 두면
128
+ * 규칙이 여러 벌이 되고, 한 홉만 보는 코드가 하나 남는 순간 **그 아래가 집계에서 조용히 빠진다.**
129
+ *
130
+ * `locationStatusOf` 와 같은 자리에 두는 이유도 같다 — 파생은 한 곳에서만 한다.
131
+ */
132
+ export interface Hierarchy {
133
+ /** 바로 아래 자리들. */
134
+ childrenOf(locationId: string): string[];
135
+ /** 상위 사슬 — 가까운 쪽부터. 마지막 원소는 자리가 아닐 수 있다(=구역). 자기 자신은 넣지 않는다. */
136
+ ancestorsOf(locationId: string): string[];
137
+ /**
138
+ * 사슬의 끝 — **자리가 아닌 최상위 소속(=구역).** 자리만으로 사슬이 끝나면(구역 미선언) `undefined`.
139
+ * 모름을 빈 문자열이나 자기 id 로 **꾸미지 않는다** — 구역 미상은 구역 0 이 아니다.
140
+ */
141
+ rollupOf(locationId: string): string | undefined;
142
+ /** 아래 전부(재귀). 자기 자신은 넣지 않는다. */
143
+ descendantsOf(locationId: string): string[];
144
+ /**
145
+ * 사슬을 올라가다 만나는 **그 단계의 가장 가까운 상위 자리** — "이 스테이션이 속한 라인" 은
146
+ * `ancestorOfLevel(st, 'ProductionLine')`. **이것이 표준 축이고 기본 질의다.**
147
+ *
148
+ * 없으면 `undefined` — 그 현장에 그 단계가 선언되지 않았다는 뜻이고, 아무 자리도 대신 내놓지 않는다.
149
+ */
150
+ ancestorOfLevel(locationId: string, level: EquipmentLevel): string | undefined;
151
+ /**
152
+ * 같은 질의를 **현장의 낱말**(`type`)로 — 단계를 아직 선언하지 않은 데이터를 위한 보조 경로다.
153
+ *
154
+ * 표준 단계가 있으면 `ancestorOfLevel` 을 쓴다. 이것은 마스터가 `level` 을 실어 주기 전까지의
155
+ * 임시 다리이고, 여기에 새 어휘를 쌓지 않는다(쌓으면 그게 방언이 된다).
156
+ */
157
+ ancestorOfType(locationId: string, type: string): string | undefined;
158
+ /**
159
+ * 이 자리에 **붙박인** 설비(`homeLocation` 기준) — 워크센터가 자리이자 설비인 경우의 이음.
160
+ * `deep` 이면 아래 자리들의 설비까지("이 라인의 설비").
161
+ *
162
+ * 지금 그 자리에 **와 있는** 설비(`location` 기준)와 다른 질문이다 — 지게차는 어디에나 와 있을 수
163
+ * 있지만 어디에도 붙박이지 않는다. 둘을 한 함수로 합치면 소속과 현재 위치가 섞인다.
164
+ */
165
+ equipmentOf(locationId: string, opts?: {
166
+ deep?: boolean;
167
+ }): string[];
168
+ }
169
+ /**
170
+ * 계층 색인을 만든다. **순환은 만들 때 잡아 오류를 낸다** — 렌더 도중에 터지는 대신 여기서 한 번에.
171
+ * 순환을 조용히 잘라 내면 롤업이 틀린 값을 내고, 그건 이 함수가 막으려는 바로 그 실패다.
172
+ */
173
+ export declare function hierarchyOf(s: {
174
+ locations: readonly {
175
+ id: string;
176
+ type?: string;
177
+ level?: EquipmentLevel;
178
+ parentId?: string;
179
+ }[];
180
+ equipment?: readonly {
181
+ id: string;
182
+ homeLocation?: string;
183
+ }[];
184
+ },
185
+ /**
186
+ * 자리가 단(`level`)을 적지 않았을 때 **타입으로 알아내는 해석기** — 카탈로그가 주입한다.
187
+ *
188
+ * 왜 주입인가: 단은 타입의 성질이라 카탈로그가 선언하는데(`TwinTypeInfo.level`), 이 파일은
189
+ * 카탈로그보다 아래에 있다(카탈로그가 이 파일을 읽는다). 여기서 카탈로그를 부르면 순환이 된다.
190
+ *
191
+ * 왜 필요한가: 실측(2026-08-14) 로케이션 82개 중 `level` 을 적은 것이 **0개**였다. 모델이 적어 줄
192
+ * 때까지 기다리면 `ancestorOfLevel` 은 계속 `undefined` 를 답한다 — 선언은 있고 답은 없는 상태다.
193
+ * 자리가 적었으면 그것이 권위이고(현장이 우리보다 자기 계층을 잘 안다), 없으면 타입이 답한다.
194
+ */
195
+ levelOfType?: (type: string) => EquipmentLevel | undefined): Hierarchy;
196
+ /**
197
+ * 자원 속성 — **ISA-95 가 세 자원에 똑같이 정의한 한 모양.**
198
+ *
199
+ * 1차 출처(B2MML v0701): `EquipmentPropertyType`(B2MML-Equipment.xsd) ·
200
+ * `PersonPropertyType`(B2MML-Personnel.xsd) · `PhysicalAssetPropertyType`(B2MML-PhysicalAsset.xsd).
201
+ * 세 타입의 요소가 동일하다 — `ID` · `Description?` · `Value(ValueType)` · `<X>PropertyChild`(재귀) ·
202
+ * `<X>ClassPropertyID`. 그래서 우리도 **자원마다 다른 모양을 만들지 않는다**(자원별 속성 타입 셋을
203
+ * 두면 그게 방언이 된다).
204
+ *
205
+ * `value`·`dataType`·`uom` 은 표준 `ValueType` 의 `ValueString`·`DataType`·`UnitOfMeasure` 다.
206
+ * 값을 **문자열로 싣는 것도 표준 그대로**다 — 단위와 데이터형이 값 옆에 붙어 있어야 소비처가
207
+ * "3" 이 초속 3m 인지 시속 3km 인지 알 수 있다(단위 없는 숫자는 거절한다는 규율의 근거).
208
+ *
209
+ * 왜 이제 계약에 넣는가: 지금까지 설비 속성(`speed`)이 **계약 밖으로 흘러** 마스터에서 호스트
210
+ * 추정기까지 `any` 로 전달됐다. 자원에 붙는 사실이 계약에 자리가 없으면 소비처마다 다르게 읽는다.
211
+ */
212
+ /**
213
+ * 이 속성 값을 **누가 말했나** — 근거를 값과 함께 나르는 규율의 속성 판(`basis`·`derived` 와 같은 결).
214
+ *
215
+ * ── 두 낱말이 한 축이다 (2026-08-18 확정) ───────────────────────────────────
216
+ * · `reference` — **외부 원천 시스템**이 준 값이다(SCADA·WMS·MES 의 마스터). 우리가 고르지 않았다.
217
+ * · `local` — **이 트윈 쪽에서 정한 값**이다. 원천이 모르는 수(계약전력·요금 단가·축전지 충전율)를
218
+ * 운영 주체가 넣은 것이고, 사람이 넣었든 대리로 AI 가 채웠든 같다 — 축은 「누가 입력
219
+ * 했나」가 아니라 **「권위가 어디 있나」**다.
220
+ *
221
+ * ── 장소 낱말과 섞지 않는다 ────────────────────────────────────────────────
222
+ * 예전 표식은 `'site'` 였다. 그런데 이 코드에서 `site` 는 **외부 시스템이 부르는 사업장**을 뜻한다
223
+ * (SAP 는 `P3`, Oracle 은 `W01` 로 같은 현장을 부른다). 그래서 「원천이 모르는 값」에 `site` 를 달면
224
+ * 이름이 내용과 정반대가 됐다 — 내가 쓴 편지를 「받은 편지함」에 넣어 둔 셈이다. `space`(트윈이 서는
225
+ * 그릇)도 답이 아니다: 그것은 **어디**에 답하는 낱말이고, 이 축은 **누가 말했나**에 답한다.
226
+ *
227
+ * ── 표식이 없으면 ──────────────────────────────────────────────────────────
228
+ * **모델을 그린 쪽이 말한 값**이다 — 참조에서 그린 모델이면 원천이고, 템플릿으로 만든 트윈이면 그
229
+ * 템플릿이다. 즉 「모른다」가 아니라 「원천 쪽」이다. 표식은 그 기본값과 다를 때 달린다.
230
+ */
231
+ export type PropertySource = 'reference' | 'local';
232
+ /**
233
+ * **ISA-95 `ValueType`** — 값·데이터형·단위 한 묶음. 1차 출처(B2MML v0700+): `ValueType`
234
+ * (`B2MML-Common.xsd`)의 `ValueString`·`DataType`·`UnitOfMeasure`.
235
+ *
236
+ * ── 왜 따로 뽑나 (2026-08-24) ────────────────────────────────────────────────
237
+ * 표준은 이 묶음을 **여러 곳에서 재사용한다** — 자원 속성(`<X>PropertyType.Value`) · 시험 측정값
238
+ * (`PropertyMeasurementType.Value`) · 공정 모수 실측(`OpSegmentDataType.Value`) · 자리 속성
239
+ * (`OperationalLocationPropertyType.Value`). 처음에는 자원 속성에만 있었고 세 필드가 그 안에 인라인
240
+ * 이었다. 두 번째 자리가 생기는 순간 **같은 매핑을 두 번 적게 되고**, 그때부터 한쪽만 고쳐지는 길이
241
+ * 열린다(단위를 한쪽에서만 필수로 만들거나, `dataType` 주석이 갈리는 식으로).
242
+ *
243
+ * 표준이 한 타입으로 둔 것을 우리도 한 타입으로 둔다. **평평하게 펴는 것**은 우리 선택이다 —
244
+ * 표준은 `Value` 를 자식 요소로 두지만, 소비처가 `p.value` 로 읽는 편이 `p.value.valueString` 보다
245
+ * 낫고 이 저장소가 이미 그렇게 하고 있었다. 그 선택을 여기 적어 둔다(되돌리려면 소비처 전부가 움직인다).
246
+ */
247
+ export interface StandardValue {
248
+ /** 표준 `Value.ValueString`. 값의 표기는 문자열이고, 뜻은 `dataType`·`uom` 이 정한다. */
249
+ value?: string;
250
+ /** 표준 `Value.DataType` — 예: `xs:double`. 미지정이면 소비처가 형을 짐작하지 않는다. */
251
+ dataType?: string;
252
+ /** 표준 `Value.UnitOfMeasure` — UN/CEFACT 공통코드(예: `MTS`·`KMH`·`CEL`). **단위 없는 물리량은 쓰지 않는다.** */
253
+ uom?: string;
254
+ }
255
+ export interface ResourceProperty extends StandardValue {
256
+ /** 속성 식별자 — 표준 `ID`. 어휘는 표준이 정하지 않으므로 우리가 한 곳에서 정한다 — 그 자리는 읽는 쪽에 있다
257
+ * (headless-twin `EQUIPMENT_PROPERTY`·`PROPERTY_EFFECTS`). 커널은 속성을 **나르기만** 한다. */
258
+ id: string;
259
+ description?: string;
260
+ /** 하위 속성 — 표준 `<X>PropertyChild`(재귀). 구조화된 속성(예: 정격/실측 묶음)을 잃지 않기 위해. */
261
+ children?: ResourceProperty[];
262
+ /** 이 개체 속성이 구체화하는 **등급 속성** — 표준 `<X>ClassPropertyID`. */
263
+ classPropertyId?: string;
264
+ /**
265
+ * 이 값을 누가 말했나(`PropertySource`) — 없으면 모델을 그린 쪽이다(대개 원천).
266
+ *
267
+ * 커널은 이 표식을 **읽지 않는다**(속성의 뜻은 `id`·`dataType`·`uom` 이 정한다). 나르기만 한다 —
268
+ * 계약에 선언해 두는 이유는, 예전에 이 표식이 **계약에 없는 필드로 얹혀 다녔기** 때문이다. 그러면
269
+ * 소비처가 `any` 로 읽고, 상태에 정말 실리는지 아무도 검사할 수 없다.
270
+ */
271
+ source?: PropertySource;
272
+ }
273
+ /**
274
+ * 자격·적격을 **검증한 시험 명세**들 — 표준 `TestSpecificationID`(`maxOccurs="unbounded"`).
275
+ *
276
+ * 1차 출처에서 **자원 타입 9개 중 9개**에 있다(`PersonType`·`PersonnelClassType`·`EquipmentType`·
277
+ * `EquipmentClassType`·`PhysicalAssetType`·`PhysicalAssetClassType`·`MaterialLotType`·
278
+ * `MaterialDefinitionType`·`MaterialClassType`). **`OperationalLocationType` 에만 없다** — 자리는
279
+ * 시험 대상이 아니기 때문이다. 그 비대칭이 표준의 판단이므로 우리도 자리에 넣지 않는다.
280
+ *
281
+ * 이 필드는 **참조만** 한다. 시험 명세 자체(`B2MML-OperationsTest.xsd`)와 시험 결과는 아직 모델에 없다
282
+ * (커버리지 ⬜). 그래서 이 값으로 **검증했다고 주장하지 않는다** — 상류가 "이 자격은 이 시험으로
283
+ * 검증됐다" 고 말해 줄 때 그 사실이 **들어올 자리**를 여는 것이 이 필드의 일이다.
284
+ * 자리가 없으면 사실이 들어오지 못한다.
285
+ */
286
+ export type TestSpecificationRefs = string[];
287
+ /**
288
+ * 시험 명세 — **참조가 가리키는 대상.**
289
+ *
290
+ * ── 왜 최소 모양인가 ──────────────────────────────────────────────────────────
291
+ * 자원 아홉 종류가 이미 이것을 **가리키고 있었다**(`TestSpecificationRefs`). 그런데 가리켜지는 것이
292
+ * 모델에 없어서 그 참조는 **허공을 가리켰다** — 값을 실을 수는 있어도 그것이 무엇인지 아무도 답할 수
293
+ * 없었고, 화면은 그 사실을 조용히 넘겼다. 대상을 세우는 것이 이 선언의 일이다.
294
+ *
295
+ * 여기 담는 것은 **셋뿐이다**: 식별자·설명·판. 표준의 명세 타입(`B2MML-OperationsTest.xsd`)에는 재는
296
+ * 속성과 합격 기준이 더 있는데, 그 필드 이름을 **아직 1차 출처와 대조하지 않았다.** 짐작해 넣으면
297
+ * 표준 이름을 발명하는 것이고 이 저장소의 가드가 그것을 잡는다 — 대조한 뒤에 늘린다.
298
+ *
299
+ * 그래서 지금 이 축은 **"무엇으로 검증했다고 하는지"를 이름으로 답한다.** 그 이상을 주장하지 않는다.
300
+ *
301
+ * ── 결과는 여기 담지 않는다 ───────────────────────────────────────────────────
302
+ * 명세는 **선언**이고 시험 결과는 **관측**이다(누가 언제 통과했나). 결과의 집은 저널이고, 축이
303
+ * 필요해지면 일정·실적과 같은 방식(`source: 'state'|'journal'`)으로 따로 낸다. 한 축에 둘을 담으면
304
+ * 선언과 관측을 구분하는 이 트윈의 뼈대가 무너진다.
305
+ */
306
+ /**
307
+ * 시험 결과 — **"이 개체가 그 시험을 통과했다"** 는 기록.
308
+ *
309
+ * ── 이것은 관측인데 왜 선언 경로로 오나 ───────────────────────────────────────
310
+ * 결과는 사실이 일어난 것이므로 관측이다. 그런데 그 사실을 **낳는 곳이 트윈이 아니다** — 자격 시험은
311
+ * 인사 시스템이, 설비 검사는 정비 시스템이 기록한다. 트윈은 그 기록을 **원본에서 받아** 안다.
312
+ * 그래서 이 값은 자원에 실려 인제스트로 들어온다(저널이 낳는 관측과 다른 길이다).
313
+ *
314
+ * ── 없으면 판정하지 않는다 ────────────────────────────────────────────────────
315
+ * 등급이 시험을 요구하는데(`ResourceClassDef.testSpecificationIds`) 그 사람에게 **결과가 아예 없으면
316
+ * 막지 않는다.** 없는 것으로 막으면 자격자가 전부 사라져 라인이 영구히 굶고, 그건 "선언한 것만 제약이
317
+ * 된다" 는 이 커널의 규율에도 어긋난다 — 원본이 결과를 주지 않는 현장에서는 그 제약이 선언되지 않은
318
+ * 것이다.
319
+ *
320
+ * **결과가 선언돼 있으면 그때부터 제약이 된다**: 불합격이거나 유효기간이 지났으면 그 등급으로 자격이
321
+ * 성립하지 않는다. 그것이 이 값을 싣는 이유다(싣지 않으면 아무 제약도 없다).
322
+ *
323
+ * ── 1차 출처와 대조했다 (2026-08-24) ─────────────────────────────────────────
324
+ * 예전에는 「필드 이름을 아직 대조하지 않았다」고 유보해 두었다. `B2MML-OperationsTest.xsd` 의
325
+ * `TestResultType` 을 읽었고, 그 결과 **둘이 갈렸다.**
326
+ *
327
+ * 표준이 준 것 `EvaluationDate` · `Expiration` · `TestableObjectID`
328
+ * · `EvaluatedCriterionResult` · `PropertyMeasurement`(unbounded)
329
+ * 우리가 좁힌 것 `result: 'pass' | 'fail'` — **표준은 판정을 열거하지 않는다**(`TextType`)
330
+ *
331
+ * 좁힘을 적어 두는 이유: HACCP 같은 규제 판정에는 「적합·부적합」 밖의 값이 온다(재검·조건부). 그때
332
+ * 이 유니온을 넓혀야 하는데, **좁힘이라고 적혀 있지 않으면 다음 사람이 「표준이 둘만 정했다」고 읽는다.**
333
+ *
334
+ * `at`·`expiresAt` 은 표준 `EvaluationDate`·`Expiration` 이다 — 이름을 바꾸지 않는다(이미 나간 계약이고,
335
+ * 개명이 사는 값은 이 주석이 대신 낸다).
336
+ */
337
+ export interface TestResult {
338
+ /** 어느 시험인가 — `TestSpecification.id` 를 가리킨다. */
339
+ specId: string;
340
+ /**
341
+ * 합격 여부 — **우리가 좁힌 것이다.** 표준 `EvaluatedCriterionResult` 는 `TextType` 이고
342
+ * 판정 어휘를 열거하지 않는다(원문 대조 2026-08-24). 넓힐 때는 이 주석과 함께 움직인다.
343
+ *
344
+ * ── 왜 선택인가 (2026-08-24) ──────────────────────────────────────────────
345
+ * **재기만 하고 판정하지 않는 원천이 정상이다.** 계측기는 값을 내고 판정은 규정이 한다. 그런데 이
346
+ * 필드가 필수이면 그런 원천의 사실을 **아예 담을 수 없다** — 판정을 지어내거나 측정값을 버리거나
347
+ * 둘 중 하나가 되고, 둘 다 이 저장소가 거절하는 것이다(결측을 값으로 바꾸지 않는다).
348
+ *
349
+ * 비어 있는 것은 **「모른다」이며 「합격」이 아니다.** `testPassedAt` 이 그것을 지킨다(거짓을 낸다).
350
+ * 기준이 선언돼 있으면 커널이 채울 수 있다(`judgeAgainstSpec`) — 그때는 `derived` 가 선다.
351
+ */
352
+ result?: 'pass' | 'fail';
353
+ /**
354
+ * 이 판정을 **우리가 냈나** — 원천이 판정하지 않아 선언된 기준으로 커널이 채운 것.
355
+ *
356
+ * 관측의 `derived` 와 같은 낱말·같은 뜻이다(낱말을 둘로 만들지 않는다). 규제 기록에서 「현장이
357
+ * 판정했다」와 「트윈이 계산했다」가 구별되지 않으면, 그 기록은 사고 뒤에 쓸 수 없다.
358
+ *
359
+ * 원천의 판정에는 표식이 없다 — **없음이 「원천이 말했다」다.**
360
+ */
361
+ derived?: boolean;
362
+ /** 언제 통과·불합격했나(ISO) — 표준 `EvaluationDate`. */
363
+ at?: ISOTime;
364
+ /**
365
+ * 언제까지 유효한가(ISO) — 자격에는 대개 유효기간이 있다.
366
+ *
367
+ * **없으면 무기한으로 본다.** 여기서 짐작으로 기한을 만들면 멀쩡한 자격자가 조용히 사라진다
368
+ * (유효기간을 모르는 것과 지난 것은 다르다).
369
+ */
370
+ expiresAt?: ISOTime;
371
+ /**
372
+ * 이 판정의 **근거가 된 측정값들** — 표준 `TestResult.PropertyMeasurement`(`maxOccurs="unbounded"`).
373
+ *
374
+ * ── 왜 판정에 값을 붙이나 (당위) ──────────────────────────────────────────
375
+ * 「부적합」이라고만 적힌 기록과 「4.2°C 를 재어 한계를 넘었다」는 기록은 **다른 물건**이다. 앞의
376
+ * 것으로는 되짚을 수 없다 — 얼마나 벗어났나, 언제였나, 다시 재면 같은 답이 나오나에 답할 수 없다.
377
+ * 규제 기록은 성질상 **사고 뒤에** 읽히므로, 근거 없는 판정은 그때 아무 일도 하지 못한다.
378
+ *
379
+ * **없어도 판정은 성립한다** — 원본이 결론만 주는 현장이 있고, 그때 값을 지어내지 않는다. 빈 것은
380
+ * `testEvidenceGaps` 가 세어 낸다(조용히 넘기지 않는다).
381
+ */
382
+ propertyMeasurements?: PropertyMeasurement[];
383
+ }
384
+ /**
385
+ * `OP_EVENT.test` 의 페이로드 — **결과가 대상을 가리킨다.**
386
+ *
387
+ * 표준(`TestResultType`)의 방향 그대로다. 상태에서는 개체 안에 계산해 두지만(소비처가 대상별로 묻는다)
388
+ * 사건에서는 가리킨다 — 그래야 대상이 무엇이든(로트·설비·사람·자리) 한 채널로 들어온다.
389
+ */
390
+ /**
391
+ * 부적합 처분의 **결정** — 좁힌 목록이다.
392
+ *
393
+ * 표준(ISA-95 Part 4 · 품질 관리 관행)이 목록을 공표하지 않으므로 우리가 정했다. 넓힐 때는 이 주석과
394
+ * 함께 움직인다. 열린 문자열로 두지 않는 이유는, 처분이 **자원에 효과를 주기** 때문이다 — 커널이 모르는
395
+ * 결정을 받으면 무엇을 해야 할지 알 수 없고, 그때 조용히 아무것도 하지 않는 것이 가장 나쁘다.
396
+ */
397
+ export declare const DISPOSITION_DECISION: readonly ["rework", "use-as-is", "scrap", "return-to-supplier", "hold"];
398
+ export type DispositionDecision = (typeof DISPOSITION_DECISION)[number];
399
+ /**
400
+ * **부적합 처분** — `OP_EVENT.disposition` 의 데이터.
401
+ *
402
+ * 근거가 된 판정을 가리킬 수 있지만 **필수가 아니다** — 판정 없이 현장 재량으로 빼는 일이 정상이다.
403
+ */
404
+ export interface DispositionFact {
405
+ /** 무엇을 처분하나 — 표준 `TestableObjectID` 와 같은 자리(로트 · 설비 · 자리에 두루 쓴다). */
406
+ subjectId: string;
407
+ decision: DispositionDecision;
408
+ /** 수량 — 일부만 처분하는 것이 정상이다. 없으면 대상 전체다. */
409
+ quantity?: number;
410
+ uom?: string;
411
+ /** 근거가 된 판정(`TestResultFact`)의 시험 명세 — 있을 때만. */
412
+ specId?: string;
413
+ /** 누가 정했나. 사람일 수도 규정일 수도 있다. */
414
+ decidedBy?: string;
415
+ /** 왜 그렇게 정했나 — 자유 서술. 커널은 판정에 쓰지 않는다. */
416
+ reason?: string;
417
+ decidedAt: ISOTime;
418
+ }
419
+ export interface TestResultFact extends TestResult {
420
+ /** 무엇을 시험했나 — 표준 `TestResult.TestableObjectID`. 물품이면 EPC. */
421
+ testableObjectId: string;
422
+ }
423
+ /**
424
+ * `OP_EVENT.complete` 의 내용 — 「이 목록이 전부다」.
425
+ *
426
+ * `axis` 를 받아 두는 이유: 지금은 물품만 이 방식이 필요하지만, 다른 목록도 같은 성질을 갖는다
427
+ * (연결된 시스템이 현재 목록을 통째로 말하는 것). 낱말을 닫으면 그때 계약을 또 고친다.
428
+ */
429
+ export interface AxisCompleteFact {
430
+ /** 어느 목록인가 — 지금은 `'items'` 만 다룬다. */
431
+ completeAxis: string;
432
+ /** 그 주기를 **시작한** 시각(ISO). 이보다 오래된 것은 이 주기에 오지 않은 것이다. */
433
+ since: ISOTime;
434
+ }
435
+ /**
436
+ * 이 시험 결과가 **이 시각에 유효한 합격인가.**
437
+ *
438
+ * 판정을 한 곳에 둔다 — 자리마다 `result === 'pass'` 와 날짜 비교를 다시 적으면 한 곳이 빠지고,
439
+ * 빠진 쪽은 만료된 자격을 통과시킨다(조용한 결함).
440
+ */
441
+ export declare function testPassedAt(r: TestResult, at?: ISOTime): boolean;
442
+ /**
443
+ * 이 개체가 **요구된 시험들을 만족하나** — 등급이 요구하고 개체가 기록을 든다.
444
+ *
445
+ * `required` 가 비면 요구가 없으므로 참이다. 요구된 시험에 **결과가 없으면 참**이다(위 머리말의 규율:
446
+ * 없는 것으로 막지 않는다). 결과가 있으면 그것이 유효한 합격이어야 한다.
447
+ */
448
+ export declare function meetsTests(required: readonly string[], results: readonly TestResult[] | undefined, at?: ISOTime): boolean;
449
+ /**
450
+ * 자원이 **지금 쓰일 수 있나, 아니면 왜 못 쓰이나** — ISA-95 `PersonnelCapability`/`EquipmentCapability`.
451
+ *
452
+ * ── 왜 계약이 이것을 소유해야 하나 ────────────────────────────────────────────
453
+ * 커널은 배정할 때 이미 이 판정을 한다(교대·고장·보류·유효기간·시험 만료). 그런데 그 규칙이 **엔진 안의
454
+ * 필터 조건으로만** 있어서, 화면은 같은 판정을 자기 코드로 다시 만들었다(`reasonOf`). 규칙이 두 벌이면
455
+ * 반드시 어긋난다 — 배정은 막는데 화면은 "가용" 이라 말하는 순간이 온다. 그 어긋남은 조용하다.
456
+ *
457
+ * 그래서 **이유까지 계약이 낸다.** 화면·예측·AI 가 같은 낱말로 말하고, 새 이유가 생기면(시험 만료가
458
+ * 그랬다) 한 곳만 늘어난다.
459
+ *
460
+ * ── 이유의 순서가 뜻이다 ──────────────────────────────────────────────────────
461
+ * 여러 이유가 겹칠 수 있다(폐기한 설비가 고장 상태로 남아 있는 것). **먼저 오는 것을 답한다** —
462
+ * "이미 모델 밖" 이 "고장" 보다 앞선다(폐기한 설비의 고장은 고칠 일이 아니다).
463
+ */
464
+ export type CapabilityReason =
465
+ /** 유효기간 전 — 아직 없는 자원(도입 예정). 기다릴 일이다. */
466
+ 'not-yet'
467
+ /** 유효기간 후 — 이미 없는 자원(폐기·퇴사). 지울 일이다. */
468
+ | 'retired'
469
+ /** 사람이 막았다(`held`) — 지시로 보류. */
470
+ | 'held'
471
+ /** 고장 — 설비만. 고칠 일이다. */
472
+ | 'down'
473
+ /** 근무·가동 시간 밖(교대 사이) — 기다리면 돌아온다. */
474
+ | 'off-shift'
475
+ /** 근무일이 아니다(휴일) — 하루 통째로 쉰다. `off-shift` 와 기다릴 시간이 다르다. */
476
+ | 'resting'
477
+ /** 요구된 시험의 결과가 만료·불합격 — 자격이 성립하지 않는다(§TestResult). */
478
+ | 'test-expired'
479
+ /** 지금 다른 일을 하고 있다 — 능력은 있고 여유가 없다. */
480
+ | 'working'
481
+ /** 쓸 수 있다. */
482
+ | 'available';
483
+ /** 가용 여부와 그 이유 — `available` 이면 `reason: 'available'`. */
484
+ export interface Capability {
485
+ available: boolean;
486
+ reason: CapabilityReason;
487
+ }
488
+ /**
489
+ * **판정 기준 하나** — 표준 `TestSpecificationCriteriaType`(`B2MML-OperationsTest.xsd`).
490
+ *
491
+ * ── 왜 이 축이 필요한가 (당위) ────────────────────────────────────────────────
492
+ * 측정값만 있으면 트윈은 「4.2」를 들고 있을 뿐이고 **그것이 괜찮은지 모른다.** 판정의 재료는
493
+ * 「값」과 「기준」 둘이다. 기준이 없으면 트윈은 조건을 알면서도 아무 말도 할 수 없다 — 계기판이 된다.
494
+ *
495
+ * ── 커널은 기준을 **정하지 않고, 재해석하지도 않는다** ──────────────────────
496
+ * 기준은 그 현장의 규정이다(식품안전·약전·사내 규격). 트윈이 정할 것이 아니고, **표현식을 커널이
497
+ * 평가하지도 않는다** — 표준의 `Expression` 이 `TextType` 이라 문법이 정의돼 있지 않다. 문법을 우리가
498
+ * 만들면 그 순간 방언이 되고, 현장의 규정과 우리 해석이 갈리는 날 어느 쪽이 옳은지 아무도 모른다.
499
+ *
500
+ * 그러면 이 값이 무슨 일을 하나: **판정에 근거가 있는지를 물을 수 있게 한다**(§`testEvidenceGaps`).
501
+ * 「부적합」이라고만 적힌 기록과 「4.2°C 를 재어 한계 4.0 을 넘었다」는 기록은 다른 물건이다.
502
+ */
503
+ export interface TestSpecificationCriterion {
504
+ /** 표준 `ID`. */
505
+ id: string;
506
+ /** 표준 `Description`. */
507
+ description?: string;
508
+ /** 표준 `Sequence` — 기준이 여럿일 때의 순서. */
509
+ sequence?: number;
510
+ /**
511
+ * 표준 `Expression`(`TextType`) — **그 현장의 표기 그대로** 나른다. 커널은 이것을 평가하지 않는다.
512
+ *
513
+ * 문법이 정의돼 있지 않으므로(자유 문자열) 파싱하면 그 순간 방언이고, 현장 규정과 우리 해석이
514
+ * 갈리는 날 어느 쪽이 옳은지 아무도 모른다. 사람이 읽는 값이다.
515
+ *
516
+ * **`expression` 과 `limit` 중 적어도 하나는 있어야 한다**(§`criterionSaysNothing`) — 둘 다 없는
517
+ * 기준은 아무 한계도 말하지 않으면서 「기준이 선언됐다」는 착각만 만든다.
518
+ */
519
+ expression?: string;
520
+ /**
521
+ * **평가 가능한 한계** — 이것이 있으면 커널이 판정할 수 있다.
522
+ *
523
+ * ── 이 축은 **표준에 없다. 우리 것이다** (2026-08-24) ──────────────────────
524
+ * B2MML 일곱 파일(Common · OperationsTest · OperationsPerformance(+Types) · OperationalLocation ·
525
+ * OperationsEvent · OperationsSchedule · WorkAlert)을 원문으로 훑었고 `Minimum`·`Maximum`·
526
+ * `Tolerance`·`Range`·`LowerLimit`·`UpperLimit` 이 **하나도 없다.** ISA-95 의 기준은
527
+ * `Expression`(TextType)과 `Result`(TextType)뿐이고, `ValueType` 도 `ValueString`·`DataType`·
528
+ * `UnitOfMeasure`·`Key` 넷이다. 즉 표준은 한계를 **사람이 읽는 문장**으로만 담는다.
529
+ *
530
+ * 그런데 트윈은 **아무도 판정하지 않을 때 판정해야 한다** — 그것이 이 축의 근거다(당위). 자유 문장
531
+ * 으로는 그것을 할 수 없다. 그래서 숫자 한계를 우리가 더한다. **더한 것이라고 적어 두는 것**이
532
+ * 이 저장소의 규율이고(§`TestResult.result` 의 좁힘과 같은 자리), 그러면 다음 사람이 이것을
533
+ * 「표준이 준 이름」으로 읽지 않는다.
534
+ *
535
+ * 이 모양으로 한계를 주는 실 원본이 있다는 것도 확인했다. **그것이 이 축을 정한 것은 아니다** —
536
+ * 판정이 필요하다는 당위가 정했고, 원본은 그것을 채울 수 있다는 사실을 확인해 준 것이다.
537
+ * 어느 배포가 무엇을 주는지는 **커널이 알 일이 아니므로 여기 적지 않는다**(연동 쪽 문서의 몫이다).
538
+ */
539
+ limit?: {
540
+ /** 이 값 미만이면 벗어난다. 없으면 아래쪽 한계가 없다(모르는 것이 아니라 없는 것이다). */
541
+ minimum?: number;
542
+ /** 이 값 초과면 벗어난다. */
543
+ maximum?: number;
544
+ /**
545
+ * 한계의 단위 — 표준 `UnitOfMeasure` 와 같은 어휘(UN/CEFACT).
546
+ *
547
+ * **없으면 짐작하지 않는다.** 관측도 단위를 말하지 않으면 같은 척도로 보고 비교하고(같은 선언에서
548
+ * 온 값들이다), **둘이 서로 다른 단위를 말하면 판정하지 않는다** — 섭씨 한계에 화씨 관측을
549
+ * 견주면 조용히 틀린다.
550
+ */
551
+ uom?: string;
552
+ };
553
+ /** 표준 `Result` — 이 기준이 만족될 때 기대하는 값(현장 표기). */
554
+ result?: string;
555
+ /** 표준 `EvaluatedPropertyID` — 이 기준이 **무엇을 재어** 판정하나. 측정값과 짝을 맞추는 키다. */
556
+ evaluatedPropertyId?: string;
557
+ }
558
+ export interface TestSpecification {
559
+ /** 표준 `ID` — 자원의 `testSpecificationIds` 가 이 값을 가리킨다. */
560
+ id: string;
561
+ /** 표준 `Description`. i18n 키일 수 있다(사람 언어는 표현 계층이 렌더한다). */
562
+ description?: string;
563
+ /** 표준 `Version` — 같은 시험의 판이 바뀌면 자격의 뜻도 바뀐다(어느 판으로 검증했나). */
564
+ version?: string;
565
+ /**
566
+ * 판정 기준들 — 표준 `TestSpecificationCriteria`(`maxOccurs="unbounded"`).
567
+ *
568
+ * 선언하지 않아도 명세는 성립한다(「무엇으로 검증했다고 하는지」를 이름으로 답하는 것이 그 최소
569
+ * 역할이었다). 선언하면 **판정에 근거가 있는지**를 물을 수 있게 된다.
570
+ */
571
+ criteria?: TestSpecificationCriterion[];
572
+ }
573
+ /**
574
+ * **측정값 하나** — 표준 `PropertyMeasurementType`(`B2MML-OperationsTest.xsd`).
575
+ *
576
+ * 표준에서 이것은 `TestResult` 의 자식이다: 판정 하나가 **자기 근거가 된 측정들을 든다.** 값의 모양은
577
+ * `ValueType` 이라 `StandardValue` 를 그대로 쓴다 — 단위 없는 물리량은 쓰지 않는다는 규율이 여기에도 산다.
578
+ */
579
+ export interface PropertyMeasurement extends StandardValue {
580
+ /** 표준 `ID`. */
581
+ id?: string;
582
+ /** 표준 `Description`. */
583
+ description?: string;
584
+ /** 표준 `TestableObjectPropertyID` — **무엇을 쟀나**(그 개체의 어느 속성인가). 기준과 짝을 맞추는 키다. */
585
+ testableObjectPropertyId?: string;
586
+ /** 표준 `MeasurementDate` — 언제 쟀나. */
587
+ measurementDate?: ISOTime;
588
+ /**
589
+ * 이 값이 **잰 것이 아니라 계산한 값**임을 밝힌다 — §`DemandWindowState.derived` 와 같은 규율.
590
+ *
591
+ * 트윈은 원본이 주지 않는 값을 계산할 수 있다(그것이 트윈의 존재 이유다). 그런데 그 수를 실측과
592
+ * **같은 자리에 같은 모양으로** 두면 화면·보고서가 그것을 잰 값으로 읽는다. 규제 기록에서 그
593
+ * 구별이 사라지는 것은 결함이 아니라 사고다. 그래서 값 옆에 종류를 함께 싣는다.
594
+ */
595
+ derived?: boolean;
596
+ }
597
+ /**
598
+ * **이 기준이 아무 한계도 말하지 않나** — 선언만 있고 내용이 없는 것을 가려낸다.
599
+ *
600
+ * 표현식(사람이 읽는 것)도 없고 숫자 한계(커널이 판정하는 것)도 없으면, 그 기준은 「기준이 선언됐다」는
601
+ * 착각만 만든다. 화면이 「관리점 셋이 걸려 있다」고 말하는데 그중 하나가 아무것도 재지 않는 것은
602
+ * 이 저장소가 거절하는 모양이다.
603
+ */
604
+ export declare function criterionSaysNothing(c: Pick<TestSpecificationCriterion, 'expression' | 'limit'>): boolean;
605
+ /**
606
+ * **이 값이 한계를 벗어났나** — 판정할 수 있을 때만 답한다.
607
+ *
608
+ * ── 세 갈래를 가린다 ────────────────────────────────────────────────────────
609
+ * `true` 벗어났다
610
+ * `false` 한계 안이다
611
+ * `undefined` **판정할 수 없다** — 숫자 한계가 없거나, 값이 수가 아니거나, 단위가 어긋난다
612
+ *
613
+ * `undefined` 를 `false` 로 뭉개지 않는 것이 이 함수의 요점이다. 「한계 안이다」와 「판정 못 했다」를
614
+ * 같은 값으로 답하면 화면이 판정하지 못한 것을 **적합으로** 보여 준다 — 규제 기록에서 그것은 사고다.
615
+ *
616
+ * ── 표현식은 읽지 않는다 ────────────────────────────────────────────────────
617
+ * `expression` 이 있어도 파싱하지 않는다(§`TestSpecificationCriterion.expression`). 숫자 한계가
618
+ * 없으면 사람이 읽을 문장은 있어도 커널은 「모른다」고 답한다.
619
+ *
620
+ * ── 단위가 어긋나면 판정하지 않는다 ─────────────────────────────────────────
621
+ * 섭씨 한계에 화씨 관측을 견주면 **오류 없이 틀린다.** 둘 다 단위를 말하지 않으면 같은 척도로 본다 —
622
+ * 같은 선언에서 온 값들이고, 그때 판정을 거부하면 단위를 적지 않는 원본에서 이 축이 영원히 침묵한다.
623
+ */
624
+ export declare function outsideLimit(criterion: Pick<TestSpecificationCriterion, 'limit'> | undefined, observed: Pick<StandardValue, 'value' | 'uom'> | undefined): boolean | undefined;
625
+ /**
626
+ * **이 판정에 근거가 있나** — 기준과 측정값의 짝을 맞춰 빈 곳을 돌려준다.
627
+ *
628
+ * ── 왜 커널이 이것을 답하나 ─────────────────────────────────────────────────
629
+ * 커널은 기준을 **평가하지 않는다**(`Expression` 은 문법이 정의되지 않은 자유 문자열). 그러나
630
+ * **짝이 맞는지는 문법을 몰라도 알 수 있다** — 그리고 그것이 이 축을 실은 이유다: 「부적합」이라고만
631
+ * 적힌 기록과 근거를 든 기록을 화면이 구별할 수 있어야 한다.
632
+ *
633
+ * unmeasured 기준은 있는데 그것을 잰 값이 없다 — **판정의 근거가 비었다**
634
+ * unmatched 측정값은 있는데 그것을 요구한 기준이 없다 — 어느 규정으로 잰 것인지 모른다
635
+ *
636
+ * 판정하지 않고 **세어서 낸다**: 이 저장소는 빈 것을 조용히 넘기지 않는다(§`ObservedReducer.unhandled`).
637
+ * 그리고 짝의 키는 표준이 준 것 그대로다 — `EvaluatedPropertyID` ↔ `TestableObjectPropertyID`.
638
+ */
639
+ export declare function testEvidenceGaps(spec: Pick<TestSpecification, 'criteria'> | undefined, result: Pick<TestResult, 'propertyMeasurements'> | undefined): {
640
+ unmeasured: string[];
641
+ unmatched: string[];
642
+ };
643
+ /**
644
+ * **선언된 기준으로 판정한다** — 원천이 판정하지 않았을 때 커널이 답을 낼 수 있는가.
645
+ *
646
+ * ── 왜 이 함수가 있나 (당위) ────────────────────────────────────────────────
647
+ * 트윈은 **아무도 판정하지 않을 때 판정해야 한다.** 값이 있고 기준이 있는데 판정이 없으면, 화면은
648
+ * 아무 말도 하지 않고 그것은 「전부 이상 없음」과 **화면상 똑같이 보인다.** 그 침묵이 이 함수가
649
+ * 없앤 것이다.
650
+ *
651
+ * ── 세 갈래로 답한다 (이 함수의 전부) ───────────────────────────────────────
652
+ *
653
+ * 'fail' 기준 하나라도 **분명히** 벗어났다
654
+ * 'pass' **말하는 기준 전부**를 판정했고 전부 안에 있다
655
+ * undefined 판정할 수 없다 — 하나라도 판정 못 한 기준이 있거나, 판정할 기준이 아예 없다
656
+ *
657
+ * `'pass'` 가 「말하는 기준 **전부**」를 요구하는 것이 이 함수의 핵심이다. 하나라도 판정하지 못했는데
658
+ * 합격이라 하면 **확인되지 않은 합격**이 되고, 규제 기록에서 그것은 결함이 아니라 사고다. 그래서
659
+ * 모르는 것이 하나라도 있으면 아무 말도 하지 않는다.
660
+ *
661
+ * 아무 한계도 말하지 않는 기준(`criterionSaysNothing`)은 **셈에서 뺀다** — 자유 문장만 적힌 기준
662
+ * 때문에 판정 가능한 것까지 침묵하면, 이 축이 그 현장에서 영원히 죽는다(고치려던 것의 반대 방향).
663
+ * 그 기준이 비어 있다는 사실은 `testEvidenceGaps` 와 `criterionSaysNothing` 이 따로 낸다 —
664
+ * **설정의 흠과 운영의 사실을 한 답에 섞지 않는다.**
665
+ *
666
+ * 커널은 `Expression` 을 **읽지 않는다**(문법이 정의되지 않은 자유 문자열). 판정은 숫자 한계로만 한다.
667
+ */
668
+ export declare function judgeAgainstSpec(spec: Pick<TestSpecification, 'criteria'> | undefined, result: Pick<TestResult, 'propertyMeasurements'> | undefined): 'pass' | 'fail' | undefined;
669
+ /**
670
+ * 유효 기간 — **ISA-95 `EffectiveStartDate` / `EffectiveEndDate`.**
671
+ *
672
+ * 1차 출처(B2MML v0701): `EquipmentType` · `PersonType` · `PhysicalAssetType` **세 개체 타입 모두**에
673
+ * 있고, 등급 타입 세 개에도 있다. 즉 표준은 이것을 **개체와 등급 양쪽의 공통 축**으로 두었다.
674
+ *
675
+ * **없을 때 무엇이 틀렸나**: 등급에만 있어서(§ResourceClassDef) **폐기한 설비가 영구히 살아 있었다.**
676
+ * 3월에 폐차한 지게차가 7월에도 배정 대상이고, 가용 대수 분모에 들어가 가동률을 낮추고, 화면에 계속
677
+ * 떴다. 도입 예정 설비도 마찬가지로 오늘부터 있는 것처럼 보였다.
678
+ */
679
+ export interface EffectivePeriod {
680
+ /** 이 시각부터 유효 — 표준 `EffectiveStartDate`. 없으면 "언제부터인지 따지지 않는다". */
681
+ effectiveStart?: ISOTime;
682
+ /** 이 시각까지 유효 — 표준 `EffectiveEndDate`. 없으면 "끝이 정해지지 않았다". */
683
+ effectiveEnd?: ISOTime;
684
+ }
685
+ /**
686
+ * 유효 기간 밖인 **이유** — 없으면(`undefined`) 유효하다.
687
+ *
688
+ * **왜 참/거짓이 아닌가**: "아직 없다" 와 "이제 없다" 는 사용자가 알고 싶은 **다른 사실**이다. 도입
689
+ * 예정 설비와 폐기한 설비가 화면에서 같은 회색으로 보이면 "왜 안 움직이나" 에 답할 수 없다 —
690
+ * 고장(`down`)·계획정지(`held`)·교대 밖(`offShift`)을 굳이 나눠 둔 것과 같은 이유다.
691
+ */
692
+ export type Effectivity = 'not-yet' | 'expired';
693
+ /**
694
+ * 이 시각에 유효 기간 밖인가 — **한 규칙**으로 개체·등급·설비↔자산 매핑을 모두 판정한다.
695
+ *
696
+ * **표준은 날짜만 정하고 "밖이면 어떻게 되는가" 는 정하지 않는다.** 그 판단은 우리 것이므로 여기 밝힌다:
697
+ * 기간 밖이면 **그 자원은 그 시각의 모델에 참여하지 않는다**(배정되지 않고, 가용 분모에 들지 않는다).
698
+ * 지우지는 않는다 — 이유를 달아 남긴다. 조용히 사라지면 "왜 없어졌나" 를 아무도 답할 수 없다.
699
+ *
700
+ * `at` 를 주지 않으면 **판단하지 않는다**(`undefined`). 모르는 시각으로 폐기를 단정하면, 시각을 안 넘긴
701
+ * 소비처 전부가 자원을 잃는다. 파싱 불가한 시각도 같다 — 짐작해 고치지 않는다.
702
+ */
703
+ /**
704
+ * 끝나는 시각을 **포함하는가** — 축마다 다르다. 규칙은 한 벌이고 규약만 밝힌다.
705
+ *
706
+ * ── 왜 이 칸이 생겼나 (2026-08-30) ────────────────────────────────────────
707
+ * 처음에는 이 판정이 한 곳뿐이었고 끝을 포함했다. 자원에는 그것이 맞다 — 폐기 일자가 있는 지게차는
708
+ * 띄엄띄엄 있고, 두 자원의 기간이 한 순간을 함께 갖는 일이 문제가 되지 않는다.
709
+ *
710
+ * 그런데 **선언은 빈틈없이 이어진다.** 8월 요금 주기가 끝나는 순간이 9월 주기가 시작하는 순간이다.
711
+ * 끝을 포함하면 그 한 순간에 두 단가가 동시에 유효해지고, 어느 것으로 계산했는지 말할 수 없다.
712
+ *
713
+ * 그때 같은 판정을 하는 함수를 하나 더 만들었다. 규칙이 두 벌이 되면 한쪽만 고쳐지는 날이 온다 —
714
+ * 이 저장소가 오늘 하루 그 부류를 여러 번 고쳤다. 그래서 함수는 하나로 두고 **규약을 인자로** 받는다.
715
+ */
716
+ export interface EffectivityOptions {
717
+ /**
718
+ * `'inclusive'`(기본) — 끝나는 시각까지 유효하다. 자원의 유효 기간이 이쪽이다.
719
+ * `'exclusive'` — 끝나는 시각은 다음 구간의 것이다. 이어지는 주기(요금·단가)가 이쪽이다.
720
+ */
721
+ end?: 'inclusive' | 'exclusive';
722
+ }
723
+ export declare function effectivityAt(p: EffectivePeriod | undefined, at?: ISOTime, opts?: EffectivityOptions): Effectivity | undefined;
724
+ /**
725
+ * 자원 **등급 정의** — ISA-95 가 세 자원에 똑같이 정의한 한 모양.
726
+ *
727
+ * 1차 출처(B2MML v0701): `PersonnelClassType`(B2MML-Personnel.xsd) · `EquipmentClassType`(B2MML-Equipment.xsd)
728
+ * · `PhysicalAssetClassType`(B2MML-PhysicalAsset.xsd). 세 타입이 공통으로 갖는 것 —
729
+ * `ID` · `Description?` · `EffectiveStartDate?` · `EffectiveEndDate?` · **`<X>ClassBaseID`(복수)** ·
730
+ * `<X>ClassProperty`(복수) · `TestSpecificationID`(복수).
731
+ *
732
+ * **정의와 참조는 이름이 다르다.** 표준은 정의를 `<X>Class` 로, 참조를 `<X>ClassID` 로 부른다. 그래서
733
+ * 우리도 정의 목록은 `personnelClasses`, 개체의 소속은 `personnelClassIds` 다 — 한 낱말로 두면
734
+ * "이 필드가 정의인가 참조인가" 를 매번 물어야 한다.
735
+ *
736
+ * **상속이 왜 필요한가**: 요구가 "생산직 2명" 인데 현장에 있는 사람이 "용접 자격자" 라면, 상속이 없으면
737
+ * 그 사람은 요구를 만족하지 못한다. 표준이 `ClassBaseID` 를 복수로 둔 이유가 이것이고(다중 상속),
738
+ * 우리는 소속을 **상속을 타고 닫아** 판정한다(`classClosure`).
739
+ */
740
+ export interface ResourceClassDef extends EffectivePeriod {
741
+ id: string;
742
+ description?: string;
743
+ /** 상위 등급들 — 표준 `<X>ClassBaseID`(복수). 순환은 `classClosure` 가 끊는다. */
744
+ baseIds?: string[];
745
+ /** 등급 속성 — 표준 `<X>ClassProperty`. 개체 속성이 `classPropertyId` 로 이것을 구체화한다. */
746
+ properties?: ResourceProperty[];
747
+ /** 이 등급의 적격을 정하는 시험 명세들 — 표준 `TestSpecificationID`. */
748
+ testSpecificationIds?: TestSpecificationRefs;
749
+ }
750
+ export declare function classClosure(directIds: readonly string[] | undefined, defs: readonly ResourceClassDef[] | undefined, at?: ISOTime): Set<string>;
751
+ /**
752
+ * 우선순위 — **ISA-95 `Priority`**(`JobOrderType`·`OperationsRequestType`, 타입은 `PriorityType` =
753
+ * `NumericType` 제한). 즉 표준은 **숫자라는 것만 정하고 방향은 정하지 않는다.**
754
+ *
755
+ * **그래서 방향은 우리가 정한다: 작은 값이 급하다(1 = 가장 급함).** 흔한 관행이고, 무엇보다
756
+ * 한쪽으로 고정해 두지 않으면 소비처마다 반대로 읽는다. 우리가 정한 규약이라는 사실을 여기 밝힌다.
757
+ *
758
+ * 미지정은 **0 이 아니라 "우선순위 없음"** 이다 — 선언한 것들 뒤에 선다(0 으로 채우면 미지정이
759
+ * 가장 급한 것이 된다).
760
+ */
761
+ export declare const PRIORITY_UNSET: number;
762
+ /** 정렬 키 — 미지정을 맨 뒤로 보낸다. 같은 우선순위는 **입력 순서**를 지킨다(결정성). */
763
+ export declare function priorityRank(p?: number): number;
764
+ /**
765
+ * 납기 대비 상태 — **파생**이다(저장하지 않는다). `locationStatusOf` 와 같은 규율.
766
+ *
767
+ * 예정 창(`endTime`)이 없으면 `undefined` — **"늦지 않았다" 가 아니라 "판단할 수 없다"** 다.
768
+ * 납기가 없는데 정시라고 말하면 그건 없는 사실을 만드는 것이다.
769
+ */
770
+ export declare function dueStatusOf(x: {
771
+ endTime?: ISOTime;
772
+ }, nowIso?: ISOTime): 'on-time' | 'late' | undefined;
773
+ /**
774
+ * 자재 수량 하나 — **ISA-95 `MaterialLot.Quantity`(`maxOccurs="unbounded"`)** 의 한 항목.
775
+ * 타입은 `QuantityValueType` = `QuantityString` + `DataType?` + `UnitOfMeasure?` + `Key?`.
776
+ *
777
+ * **왜 복수인가**: 같은 로트를 여러 단위로 함께 쓴다 — *100 EA · 250 KG · 5 CS*. 창고는 개수로 세고,
778
+ * 운송은 무게로 싣고, 주문은 케이스로 온다. 하나만 담을 수 있으면 나머지는 **버려진다.**
779
+ *
780
+ * **단위를 환산하지 않는다.** 100 EA 가 250 KG 인지는 품목마다 다른 계수이고 우리에게 없다.
781
+ * `quantityIn` 은 **선언된 단위만** 답하고, 없으면 `undefined` 다(계산해서 만들지 않는다).
782
+ *
783
+ * 값은 **숫자**로 든다 — 우리 인제스트 정본이 EPCIS 이고 `QuantityElement.quantity` 가 숫자다.
784
+ * ISA-95 는 문자열(`QuantityString`)로 두지만, 두 표준 사이에서는 EPCIS 쪽을 따른다(§정본).
785
+ */
786
+ export interface MaterialQuantity {
787
+ value: number;
788
+ /** UN/CEFACT 권고 20 코드(`EA`·`KGM`·`CS`…). 없으면 개수로 읽는다(EPCIS 규약과 같다). */
789
+ uom?: string;
790
+ /** 표준 `DataType` — 미지정이면 형을 짐작하지 않는다. */
791
+ dataType?: string;
792
+ /** 표준 `Key` — 같은 단위가 여러 번 올 때 구별하는 이름(예: 정미/총). */
793
+ key?: string;
794
+ }
795
+ /**
796
+ * 선언된 단위로 수량을 읽는다 — **환산하지 않는다.**
797
+ *
798
+ * 없으면 `undefined`: "0" 이 아니고 "계산한 값" 도 아니다. 환산 계수를 모르는데 값을 만들면
799
+ * 그 뒤 모든 계산이 거짓 위에 선다.
800
+ */
801
+ /**
802
+ * 로트의 부분 식별자를 **한 규칙으로** 만든다 — 표준 `MaterialSubLot.ID`.
803
+ *
804
+ * 시뮬(생산)과 미러(관측)가 각자 만들면 같은 부분이 다른 이름을 갖고, 두 구동이 어긋난다
805
+ * (적합성 하네스가 실제로 잡았다). 부분을 구분하는 것은 **자리**다.
806
+ */
807
+ /**
808
+ * 물품을 구별하는 키 — 직렬 물품은 `epc`, 로트의 부분은 `subLotId`(표준 `MaterialSubLot.ID`).
809
+ *
810
+ * **한 곳에서 정한다.** 소비처마다 `epc` 로 키를 잡으면 같은 로트의 두 부분이 하나로 합쳐지고,
811
+ * 그 순간 재고가 조용히 줄어든다(실제로 그랬다 — §ItemState.subLotId).
812
+ *
813
+ * ── 규약 (2026-08-21에 확정) ────────────────────────────────────────────────
814
+ * ① 물품 맵의 **키는 이 함수의 결과**다. 시뮬·미러 어느 쪽도 규칙을 인라인으로 다시 적지 않는다
815
+ * (미러가 `subLotId ?? epc` 를 세 곳에서 다시 적고 있었고, 그런 중복은 한쪽만 고쳐진다).
816
+ * ② **상태에 실리는 참조도 그 키**다(`TaskState.itemRefs` · `FlowTask.itemEpc`). 그래야 찾기가
817
+ * `get` 한 번으로 끝난다. 직렬 물품에서는 키가 곧 `epc` 이므로 대부분의 경로는 이미 그렇다.
818
+ * ③ 예외는 하나다: **원본이 준 참조**(운영 사실 유입·외부 씨앗)는 우리 키 규칙을 모른다. 그때만
819
+ * `FlowEngine.itemByRef` 의 대체 경로(전수 조회)를 지나고, 그 횟수를 `refScanCount()` 가 센다.
820
+ * 「대체 경로는 드물 것이다」를 짐작하지 않기 위해서다 — 인덱스를 얹을 근거는 그 수다.
821
+ *
822
+ * 왜 참조를 키로 통일하고 인덱스를 먼저 얹지 않았나: 키 규약이 두 갈래인 채로 인덱스를 얹으면
823
+ * **그 질문이 닫힌다**(두 갈래를 전제한 구조가 굳는다). 규약을 먼저 정하고, 남는 비용을 재서 넣는다.
824
+ */
825
+ export declare function subLotIdOf(classUri: string, location: string): string;
826
+ export declare function itemKeyOf(item: {
827
+ epc: string;
828
+ subLotId?: string;
829
+ }): string;
830
+ export declare function quantityIn(item: {
831
+ qty?: number;
832
+ uom?: string;
833
+ quantities?: MaterialQuantity[];
834
+ gtin?: string;
835
+ definitionId?: string;
836
+ }, uom?: string, definitions?: MaterialDefinitionIndex): number | undefined;
837
+ /**
838
+ * 품목 정의 — **ISA-95 `MaterialDefinition`.**
839
+ *
840
+ * 1차 출처(B2MML v0701) `MaterialDefinitionType`: `ID` · `Version` · `Description*` · `PublishedDate` ·
841
+ * `EffectiveStartDate`/`EffectiveEndDate` · `HierarchyScope` · `SpatialDefinition` ·
842
+ * **`MaterialDefinitionProperty*`** · **`MaterialClassID*`(복수)** · `MaterialLotSourceID*` ·
843
+ * `TestSpecificationID*` · `AssemblyDefinition*`.
844
+ *
845
+ * ── 없을 때 무엇이 안 됐나 ────────────────────────────────────────────────
846
+ * 로트만 관측으로 들어오고 **품목이 무엇인지는 아무 데도 없었다.** 그래서 같은 로트를 100 EA 로
847
+ * 받아도 "몇 kg 인가" 에 답할 수 없었고(§quantityIn 이 환산을 거부하는 근거), 화면은 GTIN 원문을
848
+ * 그대로 보여 줄 수밖에 없었다.
849
+ *
850
+ * ── 표준에는 환산 요소가 없다 ─────────────────────────────────────────────
851
+ * 전수 대조 결과 `MaterialDefinitionType` 에 단위 환산을 담을 **전용 요소가 없다.** 표준이 주는 것은
852
+ * **속성 주머니**(`MaterialDefinitionProperty`)뿐이다 — 설비 속도(`speed`)와 같은
853
+ * 상황이다. 그래서 자리는 표준 것을 쓰고 **이름은 우리가 정하고 밝힌다**(§MATERIAL_PROPERTY).
854
+ */
855
+ export interface MaterialDefinition extends EffectivePeriod {
856
+ /** 표준 `ID` — 우리는 **GTIN**(`urn:epc:idpat:sgtin:…`)을 쓴다. 정체성의 정본이 EPCIS 이기 때문이다. */
857
+ id: string;
858
+ description?: string;
859
+ /** 속한 등급들 — 표준 `MaterialClassID`(**복수**). 인원·자산과 같은 이유로 복수다. */
860
+ materialClassIds?: string[];
861
+ /** 품목 속성 — 표준 `MaterialDefinitionProperty`. 환산 계수가 여기 들어간다(§MATERIAL_PROPERTY). */
862
+ properties?: ResourceProperty[];
863
+ /** 적격을 검증한 시험 명세들 — 표준 `MaterialDefinition.TestSpecificationID`. */
864
+ testSpecificationIds?: TestSpecificationRefs;
865
+ /**
866
+ * 그 시험들의 **결과** — "언제 통과했고 언제까지 유효한가"(§TestResult).
867
+ *
868
+ * 참조만 있으면 "무엇으로 검증한다" 까지고, 결과가 있어야 **자격이 성립하는지**를 말할 수 있다.
869
+ * 없으면 판정하지 않는다(없는 것으로 막으면 자격자가 전부 사라진다).
870
+ */
871
+ testResults?: TestResult[];
872
+ }
873
+ /** 품목 정의 색인 — id(GTIN) → 정의. 소비처가 매번 배열을 훑지 않게. */
874
+ export type MaterialDefinitionIndex = ReadonlyMap<string, MaterialDefinition>;
875
+ /**
876
+ * **우리가 정한 품목 속성 이름** — 표준은 자리만 정하고 이름을 정하지 않는다.
877
+ *
878
+ * `perBaseUnit`: 값은 **기준 단위 하나당 그 단위의 양**이고, 단위는 속성의 `uom` 이 말한다.
879
+ * 예) 한 개(EA)가 2.5 kg 이면 `{ id: 'perBaseUnit', value: 2.5, uom: 'KGM' }`.
880
+ *
881
+ * **왜 이 모양인가**: 임의의 단위쌍 환산표(CS↔KGM↔EA…)는 품목마다 다르고 연쇄가 필요해 금세
882
+ * 커진다. 현장에서 실제로 필요한 것은 **"세는 단위에서 다른 단위로"** 이므로, 기준 단위를 축으로
883
+ * 두면 선언 한 줄로 끝난다. 기준 단위가 아닌 수량에서 출발하는 환산은 **하지 않는다**(§convertQuantity).
884
+ */
885
+ export declare const MATERIAL_PROPERTY: {
886
+ /** 기준 단위 하나당 이 단위의 양. `uom` 이 대상 단위. */
887
+ readonly perBaseUnit: "perBaseUnit";
888
+ };
889
+ /**
890
+ * 이 품목에서 `uom` 으로 가는 계수 — **선언된 것만.** 없으면 `undefined`(추정하지 않는다).
891
+ */
892
+ export declare function conversionFactorOf(def: MaterialDefinition | undefined, uom?: string): number | undefined;
893
+ /**
894
+ * 근무·비근무 구간 하나 — **ISA-95 `WorkCalendarEntry`.**
895
+ *
896
+ * 1차 출처(B2MML v0701) `WorkCalendarEntryType`: `ID` · `Description*` · `StartDateTime` ·
897
+ * `FinishDateTime` · `EntryType` · `WorkCalendarEntryChild*` · `WorkCalendarEntryProperty*`.
898
+ * 반복 규칙은 `WorkCalendarDefinitionEntryType.RecurrenceTime`/`DurationRule` 에 있다.
899
+ *
900
+ * **프레임워크와 같은 모양으로 맞췄다** — `@things-factory/work-shift` 의 `WorkShift` 는
901
+ * `fromDate`(전일 −1 · 당일 0 · 익일 +1) · `fromTime` · `toDate` · `toTime` 로 교대를 든다.
902
+ * 그것이 자정을 넘는 교대를 정확히 표현하는 모양이고, 표준의 Start/Finish 와도 맞는다. 그래서 우리도
903
+ * 같은 축을 쓴다 — 나중에 그 엔티티를 정본으로 결선할 때 **직선 매핑**이 되게(재발명 금지).
904
+ *
905
+ * 여기 담는 것은 **하루 안의 되풀이 구간**이다(달력 날짜가 아니다). 특정 날짜의 휴일·정비는 표준
906
+ * `WorkCalendarEntry` 의 `StartDateTime`/`FinishDateTime` 이 담는 영역이고 아직 없다(⬜).
907
+ *
908
+ * ── 시각의 기준을 반드시 밝혀야 한다 ──────────────────────────────────────
909
+ * 표준은 `StartDateTime`/`FinishDateTime`(**절대 시각**)을 쓴다. 우리가 시각대(`HH:MM`)로 두는 것은
910
+ * 되풀이를 간단히 적기 위한 선택이고, 그러면 **어느 기준의 06:00 인가**가 반드시 필요하다 —
911
+ * Rosarito(UTC−7)의 06:00 을 UTC 로 읽으면 7시간이 틀린다.
912
+ *
913
+ * 그래서 `TwinModelDef.utcOffsetMinutes` 가 그 기준이다. **선언하지 않으면 UTC 로 읽고, 그 사실을 여기
914
+ * 밝힌다**(조용히 가정하지 않기 위해). 프레임워크에는 이미 테넌트 시간대(`Domain.timezone`)와
915
+ * 교대→절대구간 변환(`@things-factory/work-shift` `work-shift-range`, moment-timezone)이 있으므로,
916
+ * **기준을 푸는 일은 호스트의 몫**이다(커널은 zero-dep — 시간대 데이터를 들 수 없다).
917
+ *
918
+ * 한계도 적는다: 고정 오프셋은 **일광절약시간을 따라가지 못한다.** 긴 지평선에서 정확히 하려면
919
+ * 호스트가 표준대로 **절대 구간**을 계산해 넣어야 한다(⬜ — 그때 이 필드는 필요 없어진다).
920
+ */
921
+ export interface WorkCalendarEntry {
922
+ /**
923
+ * 표준 `WorkCalendarEntryType.ID` — **교대의 이름**(`A`·`B`·`C`, `night`…).
924
+ *
925
+ * 3교대처럼 **하루를 끊김 없이 덮는** 현장에서는 이 이름이 이 선언의 거의 전부다: 설비 가용은
926
+ * 24시간 그대로이므로 "쉬는 시간" 은 생기지 않고, 대신 **"이 일이 어느 교대에 일어났나"** 를
927
+ * 말할 수 있게 된다. 그 축이 없으면 교대별 성과를 물을 수 없다(`activeShiftOf`).
928
+ */
929
+ id?: string;
930
+ /**
931
+ * **한 번뿐인 구간** — 표준 `WorkCalendarEntry.StartDateTime` / `FinishDateTime`(절대 시각).
932
+ *
933
+ * ── 표준은 둘로 나눈다 ───────────────────────────────────────────────────
934
+ * `WorkCalendarEntryType` 은 **절대 구간**이고, 되풀이 규칙은 `WorkCalendarDefinitionEntryType`
935
+ * (`RecurrenceTime`/`DurationRule`)에 있으며 항목이 `WorkCalendarDefintionEntryID` 로 그것을 가리킨다.
936
+ * 우리 `fromTime`/`toTime` 은 **되풀이 쪽**에 해당하는 우리식 단순형이고, **절대 구간 자체가 없었다.**
937
+ *
938
+ * 그래서 **휴일·연휴·특정일 정비창을 표현할 수 없었다** — 하루 안에서 반복되는 것만 말할 수 있었다.
939
+ * 8월 15일 하루를 쉰다는 사실을 `HH:MM` 으로는 적을 방법이 없다.
940
+ *
941
+ * 절대 구간은 **그 자체로 완결**이라 시각 기준(`utcOffsetMinutes`)이 필요 없다 — ISO 시각에 이미
942
+ * 들어 있다. 일광절약시간 문제도 여기서는 생기지 않는다(§4-7 이 남긴 한계가 이 항목에는 없다).
943
+ */
944
+ startDateTime?: ISOTime;
945
+ finishDateTime?: ISOTime;
946
+ /**
947
+ * **어느 요일에만** — 0 일요일 … 6 토요일. 없으면 매일이다(기존 거동).
948
+ *
949
+ * ── 표준에 되풀이 표기가 없다 (1차 출처 확인) ────────────────────────────
950
+ * `WorkCalendarDefinitionEntryType.RecurrenceTime`·`DurationRule` 은 **`CodeType`**, 즉 **불투명한
951
+ * 코드**다 — 표준은 자리만 두고 문법을 정하지 않는다. 설비 속도(`speed`)·
952
+ * 단위 환산(`MATERIAL_PROPERTY.perBaseUnit`)과 같은 상황이라, **우리가 정하고 밝힌다.**
953
+ *
954
+ * ── 없을 때 무엇을 못 했나 ───────────────────────────────────────────────
955
+ * `HH:MM` 되풀이는 **하루 안**만 말한다. 그래서 **주말에 안 도는 공장을 표현할 수 없었다** —
956
+ * 토·일을 쉬는 현장이 24시간 x 7일 도는 것으로 예측됐고, 그만큼 처리량이 부풀었다.
957
+ *
958
+ * 요일은 **선언된 시각 기준**으로 읽는다(Rosarito 의 일요일과 UTC 의 일요일은 다르다).
959
+ *
960
+ * ── 자정을 넘는 교대는 **시작한 날**로 센다 ─────────────────────────────
961
+ * 야간 교대(22:00→06:00)에 `[1..5]`(월~금)를 주면 뜻은 **"월~금에 시작한다"** 이다. 그래야
962
+ * **금요일 밤 교대가 토요일 새벽까지** 이어지고, **일요일 밤에 없는 교대가 월요일 새벽에 생기지
963
+ * 않는다.** 순간의 요일로 세면 그 둘이 정확히 반대로 틀린다.
964
+ */
965
+ daysOfWeek?: number[];
966
+ /** 시작 날짜 오프셋 — `-1` 전일 · `0` 당일 · `+1` 익일(프레임워크 `WorkShiftDateType` 와 같은 축). */
967
+ fromDayOffset?: -1 | 0 | 1;
968
+ /** 시작 시각 `HH:MM`(24시간). 분 단위까지 — 시(hour)만으로는 07:30 교대를 표현할 수 없다.
969
+ * **절대 구간(`startDateTime`)을 쓰는 항목에는 없다.** */
970
+ fromTime?: string;
971
+ toDayOffset?: -1 | 0 | 1;
972
+ /** 종료 시각 `HH:MM`. 시작보다 이르면 자정을 넘는 것으로 읽는다. */
973
+ toTime?: string;
974
+ /**
975
+ * 표준 `EntryType` — 이 구간이 **근무**인가 **비근무**(휴일·정비·휴게)인가.
976
+ * 미지정은 `working`(선언한 구간은 일하는 시간이라는 흔한 뜻).
977
+ */
978
+ entryType?: 'working' | 'non-working';
979
+ }
980
+ /** 선언된 기준의 요일(0 일요일 … 6 토요일). */
981
+ export declare function weekdayAt(ms: number, utcOffsetMinutes?: number): number;
982
+ /**
983
+ * 선언된 기준으로 **며칠째인가** — 하루 단위 셈의 경계를 정하는 데 쓴다(기간 발전량 등).
984
+ *
985
+ * 요일·분과 같은 규칙이다: 선언된 오프셋을 더한 뒤 UTC 로 읽는다. 선언이 없으면 UTC 의 하루다.
986
+ * 고정 오프셋이므로 일광절약시간을 쓰는 현장에서는 계절에 따라 한 시간 어긋난다 — 그 현장이 생기면
987
+ * 오프셋이 아니라 지역 이름(`TwinSpace.timezone`)을 커널까지 내려야 한다.
988
+ */
989
+ export declare function localDayIndexAt(ms: number, utcOffsetMinutes?: number): number;
990
+ /** 그 날의 시작(UTC ms) — `localDayIndexAt` 의 역함수. */
991
+ export declare function localDayStartMs(dayIndex: number, utcOffsetMinutes?: number): number;
992
+ export declare function inWorkCalendar(entries: readonly WorkCalendarEntry[] | undefined, minuteOfDay: number): boolean;
993
+ /**
994
+ * 이 **시각**이 근무 시간인가 — 되풀이(`HH:MM`)와 **한 번뿐인 구간**(휴일·정비창)을 함께 본다.
995
+ *
996
+ * 절대 구간은 시각 기준이 필요 없다(ISO 시각에 이미 들어 있다). 되풀이는 **선언된 기준**으로 읽는다.
997
+ * 겹치면 규칙은 하나다 — **비근무가 근무를 이긴다**(휴일이 교대 위에 얹힌다).
998
+ */
999
+ export declare function inWorkCalendarAt(entries: readonly WorkCalendarEntry[] | undefined, atMs: number, utcOffsetMinutes?: number): boolean;
1000
+ /**
1001
+ * 지금 어느 교대인가 — **근무 구간의 이름**(표준 `WorkCalendarEntryType.ID`).
1002
+ *
1003
+ * 비근무가 이기는 규칙은 여기서도 같다: 휴게·정비 중이면 **어느 교대도 아니다**(`undefined`).
1004
+ * 이름 없는 구간은 이름을 지어내지 않는다 — 교대를 나눠 놓지 않은 현장에서 `'1'` 같은 값을
1005
+ * 만들어 붙이면, 그 뒤 모든 교대별 집계가 없는 구분 위에 선다.
1006
+ *
1007
+ * 겹치는 근무 구간이 여럿이면 **먼저 선언된 것**을 답한다(선언 순서가 현장의 우선순위다).
1008
+ */
1009
+ export declare function activeShiftOf(entries: readonly WorkCalendarEntry[] | undefined, minuteOfDay: number): string | undefined;
1010
+ /**
1011
+ * 이 **시각**에 어느 교대인가 — 휴일까지 반영한다.
1012
+ *
1013
+ * 휴일에 일어난 일은 **어느 교대에도 속하지 않는다**(그날 교대는 서지 않았다). 되풀이만 보는
1014
+ * `activeShiftOf` 로는 그것을 알 수 없어, 휴일에 찍힌 기록이 평소 교대로 집계됐다.
1015
+ */
1016
+ export declare function activeShiftAt(entries: readonly WorkCalendarEntry[] | undefined, atMs: number, utcOffsetMinutes?: number): string | undefined;
1017
+ /**
1018
+ * 절대 시각(ms) → **선언된 기준의** 하루 중 분(0..1439).
1019
+ *
1020
+ * `HH:MM` 만으로는 "어느 기준의 06시" 인지 알 수 없다. 예전에 이것을 UTC 로 읽어 Rosarito(UTC−7)의
1021
+ * 06시 교대가 **7시간 틀렸다.** 선언이 없으면 UTC 이고, 그 기본값을 숨기지 않고 밝힌다.
1022
+ */
1023
+ export declare function minuteOfDayAt(ms: number, utcOffsetMinutes?: number): number;
1024
+ /**
1025
+ * 이 시각에 **근무 시간 밖인가** — 캘린더가 있으면 그것으로, 없으면 옛 `window` 로 판정한다.
1026
+ *
1027
+ * **두 구동이 이 함수를 함께 쓴다.** 예전에는 이 판정이 시뮬 안에만 있고 미러에는 **아예 없었다** —
1028
+ * 설비 델타에는 `offShift` 자리조차 없어서, 미러의 설비는 점심 휴게 중에도 "대기" 로 보였다.
1029
+ * 사람 델타에는 자리가 있었지만 그것은 **전이 순간의 값**이라, 유휴로 교대 경계를 넘긴 사람은
1030
+ * 옛 판정에 머물렀다. 시각으로만 바뀌는 사실은 실어 보낼 수 없고 **각자 계산해야** 한다.
1031
+ */
1032
+ /**
1033
+ * **왜 안 하고 있나** — 쉬는 이유를 구분한다.
1034
+ *
1035
+ * `offShift` 만으로는 화면이 "교대 밖" 이라고밖에 못 말한다. 그런데 사용자가 알고 싶은 것은
1036
+ * **휴일이라 오늘 통째로 서는 것인지**, 잠깐 휴게인지, 그냥 교대 시간이 아닌지다 — 셋은 기다릴
1037
+ * 시간도 할 일도 다르다. 신정에 24시간 멈춘 트윈이 그냥 빈 화면으로 보이면 고장으로 읽힌다.
1038
+ *
1039
+ * `'non-working'` 선언된 비근무 구간(휴일·휴게·정비창)이 덮었다.
1040
+ * `'off-hours'` 근무 구간이 하나도 안 덮었다(주말·교대 사이).
1041
+ */
1042
+ export type OffCalendarReason = 'non-working' | 'off-hours';
1043
+ export declare function offCalendarReasonAt(r: {
1044
+ window?: {
1045
+ startHour: number;
1046
+ endHour: number;
1047
+ };
1048
+ workCalendar?: WorkCalendarEntry[];
1049
+ }, ms: number, utcOffsetMinutes?: number): OffCalendarReason | undefined;
1050
+ export declare function offCalendarAt(r: {
1051
+ window?: {
1052
+ startHour: number;
1053
+ endHour: number;
1054
+ };
1055
+ workCalendar?: WorkCalendarEntry[];
1056
+ }, ms: number, utcOffsetMinutes?: number): boolean;
1057
+ /**
1058
+ * 이 자원에게 **필수인 시험 목록** — 등급 상속을 타고 닫아 모은다.
1059
+ *
1060
+ * 요구는 등급이 말하고(`ResourceClassDef.testSpecificationIds`) 기록은 개체가 든다(`testResults`).
1061
+ * 그 사이를 잇는 이 수집이 계약에 있는 이유: **두 구동이 같은 답을 내야 한다.** 시뮬과 미러가 각자
1062
+ * 모으면 "이 사람에게 무엇이 필수인가" 가 갈리고, 같은 자원을 한쪽은 막고 다른 쪽은 통과시킨다.
1063
+ */
1064
+ export declare function requiredTestsFor(directIds: readonly string[] | undefined, defs: readonly ResourceClassDef[] | undefined, at?: ISOTime): string[];
1065
+ /**
1066
+ * 자원의 **가용 능력을 판정한다** — 하나의 규칙, 하나의 자리.
1067
+ *
1068
+ * 부르는 쪽이 시각과 시간대를 준다(커널은 `now()`·`utcOffsetMinutes`, 호스트는 관측 시각). 주지 않으면
1069
+ * 시각에 달린 판정(유효기간·교대·시험 만료)은 **하지 않는다** — 모르면 판단하지 않는다는 규율이다.
1070
+ *
1071
+ * `requiredTests` 는 부르는 쪽이 등급에서 모아 넘긴다(등급 정의를 아는 것은 부르는 쪽이다).
1072
+ */
1073
+ export declare function capabilityOf(r: {
1074
+ status?: string;
1075
+ held?: boolean;
1076
+ window?: {
1077
+ startHour: number;
1078
+ endHour: number;
1079
+ };
1080
+ workCalendar?: WorkCalendarEntry[];
1081
+ testResults?: TestResult[];
1082
+ } & EffectivePeriod, ctx?: {
1083
+ at?: ISOTime;
1084
+ utcOffsetMinutes?: number;
1085
+ requiredTests?: readonly string[];
1086
+ }): Capability;
1087
+ export interface LocationState {
1088
+ id: string;
1089
+ type: string;
1090
+ /**
1091
+ * **저장 용량** — 이 자리에 동시에 놓일 수 있는 물품 수. 뜻을 바꾸지 않는다(배정 정책·포화 판정이
1092
+ * 이 뜻으로 굳어 있다). 처리 능력은 아래 `parallelism` 이 따로 말한다.
1093
+ */
1094
+ capacity?: number;
1095
+ /**
1096
+ * **동시 처리 수** — 이 자리에서 한 번에 진행될 수 있는 작업 수(대기 이론의 서버 수).
1097
+ *
1098
+ * 저장 용량과 다른 축이다: 절단 스테이션은 자재를 20개 쌓아 둘 수 있어도(`capacity`) 한 번에
1099
+ * 한 대만 깎는다(`parallelism: 1`). 예전에는 한 필드가 둘을 겸해서 그 현장을 표현할 방법이 없었고,
1100
+ * 자원만 대기 중이면 같은 자리에서 작업이 무제한 동시에 진행됐다 — 대기가 생기지 않아 병목이
1101
+ * 사라지고 예측이 낙관 쪽으로 치우쳤다.
1102
+ *
1103
+ * 미지정 = 제약 없음(기존 거동). 0 은 "처리하지 않는 자리"가 아니라 **선언 오류**로 보고 무시한다
1104
+ * — 0 으로 막을 일이라면 그 자리에 작업을 보내지 않는 것이 맞다.
1105
+ */
1106
+ parallelism?: number;
1107
+ occupancy: number;
1108
+ /** 포화도 파생 상태 — `locationStatusOf` 가 낸다(두 구동이 같은 함수를 쓴다). 용량 미상이면 없다. */
1109
+ status?: string;
1110
+ /**
1111
+ * **ISA-95 설비 계층 단계** — `ProductionLine`·`StorageZone`·`WorkCenter` 등(`EQUIPMENT_LEVEL`).
1112
+ *
1113
+ * `type` 과 다른 축이다: `type` 은 현장의 낱말(`paint-booth`·`cut-station`)이고, 이것은 **표준이
1114
+ * 정한 역할**이다. 둘을 겹쳐 두는 이유 — 연동 상대와는 표준 축으로 말하고, 화면에는 현장 낱말을
1115
+ * 보여줘야 한다. 하나만 두면 한쪽을 잃는다.
1116
+ *
1117
+ * 미지정이면 그 자리의 표준 역할을 **아직 모른다**는 뜻이다. 짐작해 채우지 않는다.
1118
+ */
1119
+ level?: EquipmentLevel;
1120
+ /**
1121
+ * **상위 자리** — 주목 롤업·구역 매핑용(마스터 계층에서 유래).
1122
+ *
1123
+ * 가리키는 대상은 **두 가지 중 하나**이고, 둘은 조회로 구별된다(`locations` 에 그 id 가 있는지):
1124
+ * - **다른 자리** — 중간 단(라인·셀·존). 계층은 여기서 깊어진다.
1125
+ * - **자리가 아닌 것** — 공간의 구역(area). 사슬의 끝이다.
1126
+ *
1127
+ * 왜 깊이가 필요한가: 최종조립 16개 스테이션이 두 라인에 속하는데 중간 단이 없으면 전부 구역에
1128
+ * 평평하게 붙고, **"라인 1의 처리량"을 물을 방법이 없다.** 현장은 라인 단위로 관리되는데 모델에
1129
+ * 그 단이 없으면 관리 단위와 모델이 어긋난다.
1130
+ *
1131
+ * **사슬은 직접 걷지 말 것** — `ancestorsOf`·`rollupOf`·`descendantsOf` 를 쓴다. 한 홉만 보는 코드는
1132
+ * 중간 단이 생기는 순간 그 아래 자리를 **집계에서 조용히 빠뜨린다**(정합성이 아니라 침묵이 문제다).
1133
+ */
1134
+ parentId?: string;
1135
+ /**
1136
+ * **전기적으로 어디서 받는가** — 이 자리에 전기를 주는 상류 자리(수전·분기).
1137
+ *
1138
+ * ── 왜 `parentId` 와 따로 두나 (2026-08-18) ─────────────────────────────────
1139
+ * 한동안 분기의 `parentId` 에 수전을 적었다. 그러면 한 필드가 두 뜻을 겸한다: **공간 포함**(이 분기는
1140
+ * 공장 안에 있다)과 **전기 상류**(이 분기는 수전에서 받는다). 겸용의 값은 조용히 새어 나간다 — 공간
1141
+ * 해석기가 수전을 「구역」으로 읽어 매 인제스트마다 거짓 경보를 냈고(「area 가 없는 것을 가리킨다」),
1142
+ * 계통을 읽는 쪽은 공간 부모를 전기 상류로 오해할 위험을 안고 있었다.
1143
+ *
1144
+ * 두 관계는 실제로 다르다: 분기는 공장 **안에** 있고(공간), 수전에서 **받는다**(전기). 한 자리가 둘을
1145
+ * 함께 가질 수 있으므로 필드도 둘이어야 한다.
1146
+ *
1147
+ * **옛 세대는 읽는 경계에서 흡수한다**(`electricalUpstreamOf`): 이 값이 없고 `parentId` 가 전기 자리를
1148
+ * 가리키면 그것을 상류로 읽는다. 저장된 모델을 고치지 않고도 두 세대가 같은 답을 낸다.
1149
+ *
1150
+ * ⚠️ **물류 흐름의 상·하류가 아니다.** 흐름 쪽에는 같은 낱말이 다른 뜻으로 있다(씬의 `downstreamRef` 는
1151
+ * 물건이 다음에 갈 곳이다). 이 값은 **전기를 어디서 받는가**이고, 물건의 이동과 무관하다.
1152
+ */
1153
+ upstreamId?: string;
1154
+ /**
1155
+ * 이 자리를 **어떻게 알게 됐는가** — `master`(원 시스템 마스터/저작이 말해 준 자리) ·
1156
+ * `observed`(이벤트에 등장해서 알게 된 자리). 소비처가 둘을 구별해야 한다: 관측으로 알게 된 자리는
1157
+ * 트윈 모델에 좌표가 없고 용량이 비어 있어 **계획에 참여하지 못한다**(그 사실을 감추지 않기 위한 표시).
1158
+ */
1159
+ origin?: 'master' | 'observed';
1160
+ /**
1161
+ * 이 자리에서 관측된 **물리량들의 마지막 값** — 속성마다 하나(§`LocationObservation`).
1162
+ *
1163
+ * 이력이 아니라 **지금 값**이다: 이력은 저널이 든다(그것이 「그때 그 방이 몇 도였나」에 답하는
1164
+ * 자리다). 여기 두는 이유는 판정이 지금 값을 보기 때문이고, 상태가 시간에 비례해 자라지 않게
1165
+ * **속성당 하나**만 든다.
1166
+ *
1167
+ * 없으면 **두지 않는다** — 빈 배열을 두면 「센서가 없는 자리」와 「아직 못 들은 자리」가 같아진다.
1168
+ */
1169
+ observations?: LocationObservation[];
1170
+ }
1171
+ /**
1172
+ * **자리에서 관측된 물리량 하나** — 냉장실 온도·습도, 세척수 유량 같은 것.
1173
+ *
1174
+ * ── 왜 트윈이 이것을 들어야 하나 (당위) ─────────────────────────────────────
1175
+ * 트윈의 판정 대상은 「물건의 상태」이고, 물건의 상태는 **조건 없이 정해지지 않는다.** 「이 로트가
1176
+ * 냉장실에 있었다」까지만 아는 트윈은 그 로트가 괜찮았는지 말할 수 없다 — 기능 하나가 없는 것이
1177
+ * 아니라 **판정의 재료가 없는 것**이다.
1178
+ *
1179
+ * 그리고 **이 조인은 트윈만 할 수 있다.** 계측 시스템은 물건의 자리 이력을 모르고, 물류 시스템은
1180
+ * 조건 이력을 모른다. 물건이 여러 시스템을 지나면 어느 원본도 전 경로를 모른다.
1181
+ *
1182
+ * ── 주인은 **자리**다. 물건이 아니다 ────────────────────────────────────────
1183
+ * 방 하나의 온도가 그 안의 물건 수백 개와 관계되고, **그 수백 개는 시간에 따라 바뀐다.** 값을 물건마다
1184
+ * 복사하면 한 사실의 사본이 수백 개 생기고 드나드는 물건마다 어긋난다. 그래서 관측은 **자리에 한 번**
1185
+ * 두고, 「그때 그 방에 있던 물건」은 시각∩자리로 파생한다(파생은 저장하지 않는다).
1186
+ *
1187
+ * 진행 중인 공정이 있어도 마찬가지다: 숙성창고에 로트가 여럿이면 숙성 작업도 여럿이고 구간이 각기
1188
+ * 다른데 **센서는 하나**다. 그 값은 어느 한 작업의 것이 아니다.
1189
+ *
1190
+ * ── 표준 근거 — 붙는 자리가 「자리」가 아니라 **설비 계층 노드**다 ────────────
1191
+ * 1차 출처: `OperationsEventType`(`B2MML-OperationsEvent.xsd`) + `OperationsRecordTemplateType`
1192
+ * (`B2MML-Common.xsd`). 붙는 자리는 `HierarchyScope.EquipmentID` 인데, ISA-95 에서는 **장소 계층이
1193
+ * 곧 설비 계층**이다(Enterprise → Site → Area → StorageZone → StorageUnit) — 냉장실은 `StorageZone`
1194
+ * 수준의 설비다. 커널의 자리가 그 노드이므로(§`LocationState.level` 이 `EquipmentLevel1Type` 근거)
1195
+ * `locationId` 가 그대로 대응된다.
1196
+ *
1197
+ * `OperationalLocationType` 에는 이 축이 **없다** — 그쪽은 나중에 들어온 공간(spatial) 개념이고
1198
+ * 마스터 속성만 든다(시계열이 아니다).
1199
+ *
1200
+ * ── 우리가 좁힌 것 ──────────────────────────────────────────────────────────
1201
+ * 표준의 기록 항목은 `InformationObject`(무엇이든)다. **그 임의성을 쓰지 않는다** — 값을 `ValueType`
1202
+ * 으로 좁힌다(§`StandardValue`). 단위 없는 물리량을 받으면 소비처가 3이 섭씨인지 화씨인지 알 수 없고,
1203
+ * 그 상태로는 어떤 판정도 못 한다.
1204
+ */
1205
+ export interface LocationObservation extends StandardValue {
1206
+ /** 표준 `HierarchyScope.EquipmentID` — 어느 자리인가. 커널의 자리 id 다. */
1207
+ locationId: string;
1208
+ /**
1209
+ * **무엇을 쟀나** — 속성 식별자. 기준과 짝을 맞추는 키다
1210
+ * (§`TestSpecificationCriterion.evaluatedPropertyId`).
1211
+ *
1212
+ * 어휘는 커널이 정하지 않는다 — 재는 것은 현장이 정한다(온도·습도·유량·중량). 커널은 나르고
1213
+ * 짝만 맞춘다. `OP_PARAM` 과 같은 규율이고, 표준도 파라미터 ID 어휘를 정하지 않는다.
1214
+ */
1215
+ propertyId: string;
1216
+ /**
1217
+ * **언제의 사실인가** — 표준 `EffectiveTimestamp`.
1218
+ *
1219
+ * 관측 시각이지 기록 시각이 아니다. 둘이 어긋나는 것이 실 연동의 정상이고, 그래서 아래를 따로 든다.
1220
+ */
1221
+ effectiveTime: ISOTime;
1222
+ /**
1223
+ * **언제까지의 사실인가** — 표준 `EffectiveEndDate`. 없으면 그 시점 하나의 값이다.
1224
+ *
1225
+ * ── 왜 구간이 필요한가 ────────────────────────────────────────────────────
1226
+ * ① **원본이 시각을 주지 않는 기록이 있다.** 수동 점검 장부는 「그날 아침」처럼 적고 시계를 적지
1227
+ * 않는다(표준도 주기 점검을 전제한다 — `RecurrenceTimeInterval`). 그때 09:00 을 찍으면 **자정을
1228
+ * 지어내는 것과 같은 발명**이다(§ADR-0039 「선언이 있으면 변환이지만 없으면 발명이다」).
1229
+ * 구간으로 적으면 참인 것만 말한다: 그날 아침의 어느 때.
1230
+ * ② **영향 로트를 찾는 데 구간이 필요하다.** 점 하나로는 며칠 머문 로트를 잡지 못한다 — 온도가 튄
1231
+ * 한 시간의 이벤트 목록에는 정작 그 방의 로트가 하나도 없을 수 있다.
1232
+ *
1233
+ * 구간을 **모르는 것**과 **점인 것**은 다르다: 점이면 이 값이 없고, 모르면 넓은 구간으로 적는다.
1234
+ */
1235
+ effectiveEndTime?: ISOTime;
1236
+ /** 언제 적혔나 — 표준 `RecordTimestamp`. 늦게 도착한 옛 관측이 최신을 덮지 않게 하는 재료다. */
1237
+ recordTime?: ISOTime;
1238
+ /** 누가 말했나 — 표준 `Source`. 계측기인지 사람이 적은 장부인지는 이 값이 답한다. */
1239
+ source?: string;
1240
+ /**
1241
+ * 이 값이 **잰 것이 아니라 계산한 값**임을 밝힌다 — §`PropertyMeasurement.derived` 와 같은 규율.
1242
+ *
1243
+ * 트윈은 원본이 주지 않는 값을 계산할 수 있다(그것이 존재 이유다). 그러나 실측과 같은 자리에 같은
1244
+ * 모양으로 두면 보고서가 그것을 잰 값으로 읽는다. 규제 기록에서 그 구별이 사라지는 것은 결함이
1245
+ * 아니라 사고다.
1246
+ */
1247
+ derived?: boolean;
1248
+ }
1249
+ /**
1250
+ * **이 시각에 이 자리에서 참인 관측** — 구간을 든 관측을 시각으로 찾는다.
1251
+ *
1252
+ * 「그때 그 방이 몇 도였나」에 답하는 첫 칸이다. 점 관측은 그 시각에만, 구간 관측은 구간 안에서 참이다.
1253
+ * 여러 개가 겹치면 **가장 늦게 시작한 것**이 답이다(정정이 나중에 온다).
1254
+ *
1255
+ * 커널은 값이 옳은지 **판정하지 않는다** — 기준의 표현식은 문법이 정의되지 않은 자유 문자열이다
1256
+ * (§`TestSpecificationCriterion`). 여기서 하는 일은 짝을 찾아 주는 것까지다.
1257
+ */
1258
+ export declare function observationAt(observations: readonly LocationObservation[] | undefined, propertyId: string, at: ISOTime): LocationObservation | undefined;
1259
+ /**
1260
+ * 물품 상태 — 표준이 담는 것을 담는다(EPCIS 2.0 / TDS).
1261
+ *
1262
+ * 세 층위가 한 모델에 들어온다: 개체(SGTIN, 일련번호까지) · **로트 클래스(LGTIN, 품번+로트)** ·
1263
+ * 품목 클래스(GTIN). 낱개 일련번호가 없고 로트로만 관리하는 자재(원자재·화학·식품)가 현장의 다수이므로
1264
+ * 로트와 수량·단위가 일급이어야 한다 — 없으면 회수·유통기한·품질 격리를 표현할 수 없다.
1265
+ */
1266
+ export interface ItemState {
1267
+ /** 식별자 — 개체(urn:epc:id:…) 또는 클래스(urn:epc:class:lgtin:… / urn:epc:idpat:…). */
1268
+ epc: string;
1269
+ /**
1270
+ * **로트의 한 부분** — 표준 `MaterialSubLot.ID`.
1271
+ *
1272
+ * ── 없을 때 무엇이 사라졌나 ──────────────────────────────────────────────
1273
+ * 비직렬 로트(LGTIN)의 키가 **로트 식별자 하나**였다. 그래서 같은 로트를 rack-1 에 100개,
1274
+ * rack-2 에 60개 관측하면 **뒤에 온 관측이 앞을 덮어 100개가 조용히 사라졌다**(합계 160 → 60).
1275
+ * 로트가 여러 자리에 나뉘어 놓이는 것은 창고에서 일상이다.
1276
+ *
1277
+ * 표준은 이 자리를 `MaterialSubLot` 으로 둔다 — 각 부분이 **자기 `ID`·`StorageLocation`·`Quantity`**
1278
+ * 를 갖는다. 그래서 우리도 부분마다 한 줄로 든다: `epc` 는 여전히 **로트의 식별자**(같은 로트의 두
1279
+ * 부분은 같은 `epc` 를 갖는다)이고, **개체를 구별하는 키는 이것**이다.
1280
+ *
1281
+ * 직렬 물품(SGTIN)은 그 자체가 유일하므로 비어 있다 — 소비처는 `subLotId ?? epc` 로 키를 잡는다.
1282
+ * **EPC 처럼 생긴 식별자를 지어내지 않는다**(그러면 파서가 깨지고 상류의 것과 구별할 수 없다).
1283
+ */
1284
+ subLotId?: string;
1285
+ /**
1286
+ * 이 로트가 **무슨 품목인가** — 표준 `MaterialLot.MaterialDefinitionID`.
1287
+ *
1288
+ * 보통은 `gtin` 이 곧 정의 id 라 비어 있다(정체성의 정본이 EPCIS 다). 상류가 GTIN 과 다른 품목 코드를
1289
+ * 쓸 때 그 사실이 **들어올 자리**가 이것이다 — 자리가 없으면 사실이 들어오지 못한다.
1290
+ */
1291
+ definitionId?: string;
1292
+ /**
1293
+ * 품목 클래스 식별자 — **URI 원문 그대로**(`urn:epc:idpat:sgtin:…` 또는 `urn:epc:class:lgtin:…`).
1294
+ * 오더의 skuMix·할당이 이 값으로 매칭하므로 뜻을 바꾸지 않는다.
1295
+ */
1296
+ gtin?: string;
1297
+ /** 품번 키(CompanyPrefix.ItemRef) — 식별자에서 파생. 로트가 달라도 같은 품번으로 모으는 축. */
1298
+ gtinKey?: string;
1299
+ /** 로트·배치 번호 — LGTIN 이면 식별자에서 파생, 직렬 개체면 ilmd 에서 온다(ilmd 는 미지원). */
1300
+ lot?: string;
1301
+ location: string;
1302
+ disposition?: string;
1303
+ /**
1304
+ * 이 로트의 **시험 결과** — 명세당 최신 하나.
1305
+ *
1306
+ * ── 로트에 「어느 기준이 걸리나」는 두지 않았다 ────────────────────────────
1307
+ * 표준 `MaterialLotType` 에는 `TestSpecificationID` 가 있다. 우리는 그것을 **로트에 두지 않는다** —
1308
+ * 이미 `TwinModelDef.materialDefinitions[].testSpecificationIds` 가 「이 품목에 무엇이 걸리나」를
1309
+ * 말하고, 물품은 `gtinKey` 로 그 선언에 닿는다. 로트마다 또 적으면 두 벌이 되고, 두 벌은 언젠가
1310
+ * 어긋난다.
1311
+ *
1312
+ * 그리고 **채우는 곳이 없는 칸은 만들지 않는다.** 로트별로 다른 기준을 말하는 원천을 만나면 그때
1313
+ * 열고, 그때는 채우는 코드와 함께 온다 — 선언만 있는 능력은 소비처에게 거짓말이 된다.
1314
+ *
1315
+ * ── 당위 (2026-08-24) ────────────────────────────────────────────────────
1316
+ * 트윈은 「이 자재를 쓸 수 있나」에 답해야 한다. 그 답의 **근거가 측정값**이다. 판정만 들고 값을
1317
+ * 버리면 「합격이라고 적혀 있다」만 남고 **왜 합격인지 되짚을 수 없다** — 규제 기록은 성질상 사고
1318
+ * 뒤에 읽히므로 그때 아무 일도 하지 못한다. 그리고 이 기록을 이어 갈 수 있는 것은 트윈뿐이다:
1319
+ * 로트는 시스템을 넘어 다니고, 판정한 시스템은 그 뒤를 모른다.
1320
+ *
1321
+ * ── 표준에서 왜 여기 있나 (원문 대조) ─────────────────────────────────────
1322
+ * `MaterialLotType`(B2MML-Material.xsd)이 드는 것은 `TestSpecificationID`(어느 기준)와
1323
+ * `Disposition`(판정)이고, **시험 결과는 로트 안에 없다.** 결과는 `TestResultType` 이라는 별개
1324
+ * 기록이고 `TestableObjectID` 로 **대상을 가리킨다**(B2MML-OperationsTest.xsd).
1325
+ *
1326
+ * **그것이 우리가 좁힌 자리다**: 우리 상태는 대상별 투영이라(소비처가 「이 물품은?」을 묻는다) 가리키는
1327
+ * 기록을 개체 안에 계산해 둔다. 같은 사실이고 방향만 다르다 — 사건에서는 표준 그대로 대상을 가리킨다
1328
+ * (§`OP_EVENT.test`). 자원에서 이미 같은 좁힘을 했다(`EquipmentState.testResults`).
1329
+ *
1330
+ * ── 명세당 하나인 이유 ────────────────────────────────────────────────────
1331
+ * 이력을 들면 상태가 **계측 주기로 자란다**(품목 100만 기준에서는 그것이 곧 벽이다). 「그때 무엇을
1332
+ * 쟀나」는 저널이 답하는 물음이고, 상태가 답하는 것은 「지금 이 로트의 자격이 무엇이냐」다.
1333
+ * 자리의 관측을 속성당 하나만 든 것과 같은 규율이다(§`LocationState.observations`).
1334
+ */
1335
+ testResults?: TestResult[];
1336
+ /** 소속 물류단위(팔레트 SSCC 등) — AggregationEvent 로 맺어진다. 3D 적재 표현의 재료. */
1337
+ parent?: string;
1338
+ /**
1339
+ * 이 물류단위를 싣고 있는 **반복사용 자산**(GRAI 팔레트 등). `parent`(물류단위 소속)와 다른 축이다:
1340
+ * `parent` 는 "무엇에 담겼나"(SSCC), 이것은 "무엇에 실렸나"(GRAI). 자산을 쓰지 않는 현장은 비어 있다.
1341
+ */
1342
+ carriedBy?: string;
1343
+ /** 수량 — 비직렬(클래스) 물품의 개수·중량. 개체 물품은 1. */
1344
+ qty?: number;
1345
+ /** 수량 단위(UN/ECE Rec 20 코드: EA·KGM 등). 없으면 개수로 읽는다. */
1346
+ uom?: string;
1347
+ /**
1348
+ * **선언된 모든 수량** — 표준 `MaterialLot.Quantity`(복수). 같은 로트가 100 EA 이면서 250 KG 다.
1349
+ *
1350
+ * `qty`/`uom` 은 그중 **계산에 쓰는 주 수량**이다(배정·이행 산식이 이 값으로 굳어 있다).
1351
+ * `quantities` 는 **받은 것 전부**이고 주 수량도 그 안에 든다 — 읽을 때는 `quantityIn` 을 쓴다.
1352
+ */
1353
+ quantities?: MaterialQuantity[];
1354
+ expiry?: number;
1355
+ /**
1356
+ * 개체·로트 마스터데이터 원문 — 표준이 속성 이름을 정의하지 않으므로(상위 문서 소관) **받은 것을
1357
+ * 그대로 들고 있는다.** 도메인이 자기 어휘로 읽을 수 있고, 우리가 모르는 속성도 잃지 않는다.
1358
+ */
1359
+ ilmd?: Record<string, unknown>;
1360
+ }
1361
+ /**
1362
+ * 운영·키네마틱 모션 — State 이원 모델의 "연속" 절반.
1363
+ * 백엔드는 이동 시작 시 이 값(from/to/duration)만 방출하고, UI 는 progress 를
1364
+ * 로컬 프레임레이트로 보간한다(매 tick 통신 아님). 좌표는 커널 토폴로지 수준이 아니라
1365
+ * 보드 바인딩에서 자리→좌표로 해석. 상세: design/simulation/execution-model.md §4·§5
1366
+ */
1367
+ export interface EquipmentMotion {
1368
+ fromNode: string;
1369
+ toNode: string;
1370
+ startedAtSimMs: number;
1371
+ durationMs: number;
1372
+ progress: number;
1373
+ elapsedMs: number;
1374
+ }
1375
+ /**
1376
+ * 설비 종합효율(OEE = Availability × Performance × Quality) — 도메인-일반 설비 메트릭.
1377
+ * 규약(단일 컨벤션): PlannedTime=설비 존재 sim 시간. Availability 손실=셋업(+고장), Performance 손실=기아(유휴),
1378
+ * Quality 손실=불량. Availability=(planned−setup)/planned · Performance=run/(planned−setup) · Quality=good/(good+scrap).
1379
+ * → OEE=(run/planned)×quality. 세 인자가 손실 위치(셋업·기아·불량)를 분해해 드러낸다.
1380
+ */
1381
+ export interface OeeMetrics {
1382
+ availability: number;
1383
+ performance: number;
1384
+ quality: number;
1385
+ overall: number;
1386
+ runMs: number;
1387
+ setupMs: number;
1388
+ downMs: number;
1389
+ idleMs: number;
1390
+ goodCount: number;
1391
+ scrapCount: number;
1392
+ }
1393
+ export interface EquipmentState extends EffectivePeriod {
1394
+ id: string;
1395
+ kind: string;
1396
+ /** **지금 어디에 있나.** 운반 작업이 끝나면 도착 자리로 옮겨진다(제자리 작업은 안 움직인다). */
1397
+ location?: string;
1398
+ /**
1399
+ * **어디에 속하나** — 붙박인 자리(마스터의 `homeLocationId`). `location` 과 다른 질문이다.
1400
+ *
1401
+ * 이 필드가 **고정 설비와 이동 설비를 구분한다** — 새 타입 플래그 없이:
1402
+ * - 도장기·용접로봇은 평생 그 자리에 있다 → `location === homeLocation` 가 항상 성립.
1403
+ * - 지게차·호슬러는 돌아다닌다 → 둘이 갈린다. 그래도 소속은 `homeLocation` 하나로 안정적이다.
1404
+ *
1405
+ * 그래서 "이 라인의 설비 가동률" 은 `homeLocation` 로 묻고(소속), "지금 이 자리에 누가 와 있나" 는
1406
+ * `location` 으로 묻는다. 예전에는 소속이 적재 시점에 `location` 초기값으로 소비되고 **버려졌다** —
1407
+ * 그래서 이동 설비가 한 번 움직이면 원래 소속을 아무도 알 수 없었다.
1408
+ */
1409
+ homeLocation?: string;
1410
+ status: string;
1411
+ taskId?: string;
1412
+ motion?: EquipmentMotion;
1413
+ oee?: OeeMetrics;
1414
+ held?: boolean;
1415
+ /**
1416
+ * 교대 밖이라 지금 일하지 않는다 — 고장(down)·계획정지(held)와 **다른 이유**다.
1417
+ * 셋을 합치면 "왜 안 움직이나" 에 답할 수 없다(고쳐야 하나·풀어야 하나·기다려야 하나).
1418
+ */
1419
+ offShift?: boolean;
1420
+ /** generating — 만든 전력·역송(kW). */
1421
+ generatedKW?: number;
1422
+ exportKW?: number;
1423
+ /**
1424
+ * generating — **지금까지 만든 양**(kWh, 계기 적산값).
1425
+ *
1426
+ * 연결된 시스템이 준 값 그대로다(우리가 kW 를 적분한 값이 아니다). 「오늘 얼마나 발전했나」는 두 시점의 차이로
1427
+ * 얻는다 — 그 구간을 무엇으로 잡을지는 현장이 정하는 것이므로 커널이 구간을 정해 두지 않는다.
1428
+ */
1429
+ generatedKWh?: number;
1430
+ /**
1431
+ * 적산이 **되돌아간 시각** — 계기가 뒤로 간 것을 본 마지막 시점.
1432
+ *
1433
+ * 인버터를 교체하면 적산이 0부터 다시 오른다. 그것을 모르면 차분을 구하는 쪽이 그 계단을 「음의
1434
+ * 발전」으로 읽거나, 반대로 그 앞의 값을 잃는다. 되돌아간 사실을 **우리가 메우지 않고 표시만** 한다 —
1435
+ * 없던 발전량을 지어내는 것보다 「이 시점을 건너 계산하지 말라」고 말하는 것이 정직하다.
1436
+ *
1437
+ * 되돌아간 적이 없으면 이 칸이 없다.
1438
+ */
1439
+ generatedKWhResetAt?: ISOTime;
1440
+ /**
1441
+ * 지금 든 적산이 **무엇부터 쌓인 것인가** — 원본이 말했을 때만 있다 (2026-08-28).
1442
+ *
1443
+ * 이 값이 있으면 차분을 구하는 쪽이 구간을 안다. 그리고 값이 줄었을 때 그것이 계기의 이상인지
1444
+ * (기준점 그대로) 구간이 새로 시작한 것인지(기준점이 나아갔다) 갈릴 수 있다.
1445
+ */
1446
+ generatedKWhSince?: ISOTime;
1447
+ /**
1448
+ * `generatedKWhResetAt` 이 **사실인가 짐작인가**.
1449
+ *
1450
+ * 'declared' 기준점을 받았고 그것이 그대로인데 값이 줄었다 — 계기의 이상이다
1451
+ * 'inferred' 기준점을 받지 못했고 값이 줄었다 — 계기 교체인지 구간 경계인지 **모른다**
1452
+ *
1453
+ * 예전에는 둘을 구별하지 않고 언제나 「되돌아갔다」로 적었다. 그러면 하루 누적을 주는 원본에서 매일
1454
+ * 자정이 계기 교체로 보인다. 짐작을 사실처럼 적지 않기 위한 칸이다.
1455
+ */
1456
+ generatedKWhBasis?: 'declared' | 'inferred';
1457
+ /**
1458
+ * 지금 든 적산이 **어떤 누적인가** — 원본이 말하지 않으면 `'unknown'` 이다(칸이 비지 않는다).
1459
+ *
1460
+ * 「모른다」를 값으로 두는 이유: 이 칸이 비어 있으면 소비처가 그 수를 평생치로 읽는다. 그러면 하루치를
1461
+ * 주는 발전소의 「지금까지 낸 양」이 오늘치가 되고, 아무도 그것을 알아채지 못한다.
1462
+ */
1463
+ generatedKWhAccumulation?: 'lifetime' | 'daily' | 'monthly' | 'billing' | 'unknown';
1464
+ /**
1465
+ * **끝난 기간의 발전량**(kWh) — 가장 최근에 마감된 한 기간의 총량.
1466
+ *
1467
+ * ── 왜 이 칸이 필요한가 (2026-08-28) ──────────────────────────────────────
1468
+ * ISO 50001 의 성과지표(EnPI)와 기준선은 「기간당 에너지」로 정의된다. 태양광 성능비도 분자가 기간
1469
+ * 발전량이다. 적산값 하나만 두면 그 어느 것도 계산할 수 없다.
1470
+ *
1471
+ * 기간의 경계는 **둘 중 하나에서** 온다. 어느 쪽이었는지 사건에 적는다(§`EnergyGenerationPeriodData`).
1472
+ *
1473
+ * 원본의 선언 `generatedKWhSince` 가 나아갔다 — 그 시각이 곧 앞 기간의 끝이다
1474
+ * 현장의 선언 `TwinModelDef.utcOffsetMinutes` 로 정한 그 지역의 자정
1475
+ *
1476
+ * 커널이 시간대를 짐작하는 일은 없다. 선언이 없으면 UTC 로 읽고, 그것이 선언되지 않았다는 사실은
1477
+ * 트윈 정의에 남는다(§`utcOffsetMinutes`).
1478
+ *
1479
+ * 총량은 계산이 아니라 관측이다 — 하루치를 주는 원본은 마지막으로 본 값 그대로이고, 평생 적산을 주는
1480
+ * 원본은 그날 처음 본 값과의 차이다. 관측이 미치지 못한 구간은 사건이 시각으로 밝힌다.
1481
+ *
1482
+ * 같은 값이 사건으로도 나간다(`ENERGY_EVENT.generationPeriod`). 상태에만 두면 지난 기간들을 합할 수
1483
+ * 없다.
1484
+ */
1485
+ generatedKWhLastPeriod?: number;
1486
+ /** 그 기간이 끝난 시각. 원본이 새 기준점을 말했으면 그 시각이고, 아니면 되돌아감을 본 시각이다. */
1487
+ generatedKWhLastPeriodEnd?: ISOTime;
1488
+ /**
1489
+ * 그 기간에서 **처음·마지막으로 관측한 시각** — 기간의 경계와 다르면 그만큼 재지 않았다.
1490
+ *
1491
+ * 이 둘이 없으면 상태만 읽는 화면이 덜 잰 값을 온전한 값으로 읽는다. 얼마나 어긋나면 문제인지는
1492
+ * 현장이 정할 일이므로 커널이 판정하지 않고 시각을 그대로 낸다(판정을 두 벌 만들지 않는다).
1493
+ */
1494
+ generatedKWhLastPeriodObservedFrom?: ISOTime;
1495
+ generatedKWhLastPeriodObservedTo?: ISOTime;
1496
+ /**
1497
+ * 전기 계측 — 전압(V)·전류(A). 출력이 0 일 때 **왜 0 인지**를 이 값들이 말한다
1498
+ * (§`EnergyEquipmentData.dcVoltage`).
1499
+ */
1500
+ dcVoltage?: number;
1501
+ dcCurrent?: number;
1502
+ acVoltage?: number[];
1503
+ acCurrent?: number[];
1504
+ /** storing — 충전율(%)·충전·방전(kW). 충전과 방전을 나눈다(손실·수명 판단이 그 둘을 구별한다). */
1505
+ soc?: number;
1506
+ chargeKW?: number;
1507
+ dischargeKW?: number;
1508
+ /** curtailable — 줄일 수 있나, 그리고 최소 유지(kW). 그 아래로 내리면 공정이 죽는다. */
1509
+ curtailable?: boolean;
1510
+ minKW?: number;
1511
+ /** switching — 개폐 위치(IEC 61850 `Pos`). 과도·불량을 열림/닫힘으로 반올림하지 않는다. */
1512
+ position?: 'open' | 'closed' | 'intermediate' | 'bad';
1513
+ /**
1514
+ * 위 값들을 **언제 들었나** — 값과 함께 다녀야 하는 사실.
1515
+ *
1516
+ * 에너지에서 가장 위험한 화면은 멈춘 값을 지금 값처럼 보여 주는 것이다. 통신이 끊긴 설비는 마지막
1517
+ * 상태를 그대로 들고 있고, 시각이 없으면 화면은 그것을 「지금」으로 그린다. 값만 적고 시각을
1518
+ * 버리면 그 판단을 아무도 할 수 없다.
1519
+ */
1520
+ measuredAt?: ISOTime;
1521
+ /**
1522
+ * 유효 기간 밖이라 이 시각의 모델에 없다 — **네 번째 이유**(§Effectivity).
1523
+ * 도입 예정(`not-yet`)과 폐기(`expired`)를 구별한다. 유효하면 값이 없다.
1524
+ */
1525
+ effectivity?: Effectivity;
1526
+ /**
1527
+ * **왜 쉬나** — `offShift` 가 참일 때만 있다(§OffCalendarReason).
1528
+ * `non-working` 휴일·휴게·정비창 · `off-hours` 주말·교대 사이. 셋은 기다릴 시간도 할 일도 다르다.
1529
+ */
1530
+ offShiftReason?: OffCalendarReason;
1531
+ /**
1532
+ * 지금 어느 교대인가 — 표준 `WorkCalendarEntry.ID`(§activeShiftOf).
1533
+ *
1534
+ * **파생값이다** — 델타로 실어 보내지 않는다(교대 경계는 이벤트 없이 시각만으로 넘어간다).
1535
+ * 3교대처럼 하루를 끊김 없이 덮는 현장에서 이 값이 **교대별 성과를 물을 수 있는 유일한 축**이다.
1536
+ */
1537
+ shift?: string;
1538
+ /** 이 자원을 어떻게 알게 됐는가 — LocationState.origin 과 같은 뜻(성장 정책을 한 규칙으로 선언). */
1539
+ origin?: 'master' | 'observed';
1540
+ /**
1541
+ * 자원 속성 — 표준 `EquipmentProperty`. 속도(`speed`)처럼 **소비처가 읽는 사실**이
1542
+ * 여기 실린다. 지금까지 계약 밖으로 흘러 호스트까지 `any` 로 전달됐다(§ResourceProperty).
1543
+ */
1544
+ properties?: ResourceProperty[];
1545
+ /** 적격을 검증한 시험 명세들 — 표준 `Equipment.TestSpecificationID`(§TestSpecificationRefs). */
1546
+ testSpecificationIds?: TestSpecificationRefs;
1547
+ /**
1548
+ * 그 시험들의 **결과** — "언제 통과했고 언제까지 유효한가"(§TestResult).
1549
+ *
1550
+ * 참조만 있으면 "무엇으로 검증한다" 까지고, 결과가 있어야 **자격이 성립하는지**를 말할 수 있다.
1551
+ * 없으면 판정하지 않는다(없는 것으로 막으면 자격자가 전부 사라진다).
1552
+ */
1553
+ testResults?: TestResult[];
1554
+ /**
1555
+ * **왜 지금 쓰일 수 없나** — 판정과 사유를 계약이 낸다(`capabilityOf` · `CapabilityReason`).
1556
+ *
1557
+ * 상태에 실어 보내는 이유: 이 판정은 **시각과 등급 정의를 함께 알아야** 나온다(유효기간·교대·자격
1558
+ * 만료). 소비처는 그 둘을 갖고 있지 않다 — 화면은 시뮬 시각을 모르고, 호스트는 등급 상속을 타고
1559
+ * 필수 시험을 모으는 규칙을 모른다. 그래서 예전에는 소비처가 플래그를 보고 짐작했고, 자격이 만료된
1560
+ * 사람이 `대기` 로 보였다. 대기 중인 것은 맞지만 **쓸 수 있는 것은 아니다** — "대기 7명" 이 조용히
1561
+ * 틀린 숫자가 됐다.
1562
+ *
1563
+ * 배정이 쓰는 판정과 **같은 함수의 결과**다(규칙 한 벌). 사유의 순서도 계약이 정한다.
1564
+ */
1565
+ capability?: Capability;
1566
+ }
1567
+ /**
1568
+ * 사람 — **ISA-95 `Person`.** 설비와 다른 자원 종류다.
1569
+ *
1570
+ * 왜 설비로 합치지 않는가: 사람은 고장 나지 않고(MTBF), 설비종합효율로 평가하지 않으며,
1571
+ * **등급(자격)과 교대로 산다.** 같은 그릇에 담으면 설비의 어휘(고장·수리·OEE)가 사람에게 붙고
1572
+ * 사람의 어휘(등급·교대·투입 인원)가 설비에 붙는다 — 둘 다 거짓이 된다.
1573
+ *
1574
+ * 그리고 **인원은 현장에서 가장 자주 부족한 자원**이다. 모델에 없으면 "사람을 두 명 더 넣으면
1575
+ * 어떻게 되나" 를 물을 수 없고, 사람이 만들어 내는 줄이 예측에서 통째로 사라진다.
1576
+ */
1577
+ export interface PersonState extends EffectivePeriod {
1578
+ id: string;
1579
+ /**
1580
+ * 소속 등급들 — **ISA-95 `Person.PersonnelClassID`, `maxOccurs="unbounded"`**(B2MML-Personnel.xsd).
1581
+ *
1582
+ * **복수인 것이 표준이고, 그것이 표준의 자격 표현이다.** 한 사람이 용접 자격과 지게차 자격을 함께
1583
+ * 갖는다 — 예전에는 이 필드가 문자열 하나여서 그 사람을 두 작업 중 하나에만 배정할 수 있었다
1584
+ * (자격 하나를 고르면 나머지 자격이 사라지는 모델).
1585
+ *
1586
+ * 배정은 개인 지목이 아니라 **등급으로 요구**되고(`OpPersonnelSpecification` = ClassID + Quantity),
1587
+ * 사람이 그 등급 중 하나를 **포함**하면 자격이 성립한다.
1588
+ */
1589
+ personnelClassIds?: string[];
1590
+ /** 'idle' | 'busy'. 고장(down)이 없다 — 사람은 그렇게 모델링하지 않는다. */
1591
+ status: string;
1592
+ taskId?: string;
1593
+ /** 교대 밖 — 자원(EquipmentState.offShift)과 같은 뜻. */
1594
+ offShift?: boolean;
1595
+ /**
1596
+ * 유효 기간 밖 — 설비와 같은 뜻(§Effectivity). 사람에게는 **입사 전·퇴사 후**가 이것이다
1597
+ * (자격 만료는 등급 쪽 유효 기간이다 — 사람은 남고 자격만 끊긴다).
1598
+ */
1599
+ effectivity?: Effectivity;
1600
+ /**
1601
+ * 지금 어느 교대인가 — 표준 `WorkCalendarEntry.ID`(§activeShiftOf).
1602
+ *
1603
+ * **파생값이다** — 델타로 실어 보내지 않는다(교대 경계는 이벤트 없이 시각만으로 넘어간다).
1604
+ * 3교대처럼 하루를 끊김 없이 덮는 현장에서 이 값이 **교대별 성과를 물을 수 있는 유일한 축**이다.
1605
+ */
1606
+ shift?: string;
1607
+ /**
1608
+ * 지금 어디에 있나 — **표준 `Person.OperationalLocation`**(B2MML-Personnel.xsd, `ResourceLocationType`).
1609
+ *
1610
+ * 표준은 사람에게 위치를 준다. 우리 모델에는 없어서 "이 라인에 몇 명 있나" 를 물을 수 없었다.
1611
+ * 커널은 사람을 움직이지 않으므로(작업이 사람을 부른다) 이 값은 **마스터가 말해 주거나 관측으로
1612
+ * 들어온다** — 그래서 사람 델타에도 자리가 있다(상태⊆이벤트).
1613
+ */
1614
+ location?: string;
1615
+ /** 자원 속성 — 표준 `PersonProperty`. 자격증·숙련 등급 같은 사실이 여기 들어간다(§ResourceProperty). */
1616
+ properties?: ResourceProperty[];
1617
+ /** 자격을 검증한 시험 명세들 — 표준 `Person.TestSpecificationID`(§TestSpecificationRefs). */
1618
+ testSpecificationIds?: TestSpecificationRefs;
1619
+ /**
1620
+ * 그 시험들의 **결과** — "언제 통과했고 언제까지 유효한가"(§TestResult).
1621
+ *
1622
+ * 참조만 있으면 "무엇으로 검증한다" 까지고, 결과가 있어야 **자격이 성립하는지**를 말할 수 있다.
1623
+ * 없으면 판정하지 않는다(없는 것으로 막으면 자격자가 전부 사라진다).
1624
+ */
1625
+ testResults?: TestResult[];
1626
+ /**
1627
+ * **왜 지금 쓰일 수 없나** — 판정과 사유를 계약이 낸다(`capabilityOf` · `CapabilityReason`).
1628
+ *
1629
+ * 상태에 실어 보내는 이유: 이 판정은 **시각과 등급 정의를 함께 알아야** 나온다(유효기간·교대·자격
1630
+ * 만료). 소비처는 그 둘을 갖고 있지 않다 — 화면은 시뮬 시각을 모르고, 호스트는 등급 상속을 타고
1631
+ * 필수 시험을 모으는 규칙을 모른다. 그래서 예전에는 소비처가 플래그를 보고 짐작했고, 자격이 만료된
1632
+ * 사람이 `대기` 로 보였다. 대기 중인 것은 맞지만 **쓸 수 있는 것은 아니다** — "대기 7명" 이 조용히
1633
+ * 틀린 숫자가 됐다.
1634
+ *
1635
+ * 배정이 쓰는 판정과 **같은 함수의 결과**다(규칙 한 벌). 사유의 순서도 계약이 정한다.
1636
+ */
1637
+ capability?: Capability;
1638
+ }
1639
+ /**
1640
+ * 물리 자산 — **ISA-95 `PhysicalAsset`, GS1 `GRAI`(반복사용 자산).**
1641
+ *
1642
+ * 왜 물품(Material)과 따로 두는가: **SSCC 와 GRAI 는 다른 것**이다. SSCC 는 *물류단위*(그 팔레트에
1643
+ * 실린 화물 한 덩어리)이고 GRAI 는 *돌아오는 팔레트 자체*다. 같은 GRAI 팔레트가 오늘은 SSCC 999 를,
1644
+ * 내일은 다른 SSCC 를 싣는다. 둘을 합치면 **팔레트 회수·풀링을 표현할 수 없고**(팔레트가 화물과 함께
1645
+ * 사라진다), EPCIS 조립 이벤트의 `parentID`(=물류단위)도 뜻이 흐려진다.
1646
+ *
1647
+ * 그리고 빈 팔레트 부족은 현장의 실제 제약이다 — 자산이 없어 작업이 못 나가는 일이 사람 부족만큼 잦다.
1648
+ */
1649
+ export interface AssetState extends EffectivePeriod {
1650
+ id: string;
1651
+ /** 자산 등급 — pallet · rack · bin · trailer 등. 도메인 소유(코어는 강제하지 않는다). */
1652
+ /**
1653
+ * 속한 등급들 — **표준 `PhysicalAsset.PhysicalAssetClassID`, `maxOccurs="unbounded"`.**
1654
+ * 인원과 같은 이유로 복수다(팔레트가 'euro-pallet' 이면서 'food-grade' 일 수 있다).
1655
+ * 요구 쪽(`physicalAssetSpecification`)은 표준대로 등급 하나 + 수량이다.
1656
+ */
1657
+ assetClassIds?: string[];
1658
+ /** 지금 있는 자리. */
1659
+ location?: string;
1660
+ /** 'idle' | 'in-use'. 고장·OEE 로 평가하지 않는다(설비가 아니다). */
1661
+ status: string;
1662
+ /** 지금 이 자산이 잡혀 있는 작업. */
1663
+ taskId?: string;
1664
+ /**
1665
+ * 지금 싣고 있는 물류단위(SSCC 등) — **자산과 화물의 연결.**
1666
+ * 비어 있으면 빈 팔레트다(회수 대상이자 다음 출고의 재료).
1667
+ */
1668
+ carrying?: string;
1669
+ /** 자원 속성 — 표준 `PhysicalAssetProperty`(§ResourceProperty). */
1670
+ properties?: ResourceProperty[];
1671
+ /** 적격을 검증한 시험 명세들 — 표준 `PhysicalAsset.TestSpecificationID`(§TestSpecificationRefs). */
1672
+ testSpecificationIds?: TestSpecificationRefs;
1673
+ /**
1674
+ * 유효 기간 밖 — 설비와 같은 뜻(§Effectivity). 자산에서는 **폐기한 팔레트**가 풀에서 빠지는 것이다.
1675
+ * 빠뜨리면 회수 대상 수가 실제보다 많게 잡히고, 빈 팔레트 부족이 보이지 않는다.
1676
+ */
1677
+ effectivity?: Effectivity;
1678
+ /** 그 시험들의 결과 — 사람과 같은 뜻(§TestResult). 검사에서 떨어진 팔레트는 배정에서 빠진다. */
1679
+ testResults?: TestResult[];
1680
+ /** 왜 지금 쓰일 수 없나 — 사람·설비와 **같은 판정**(§PersonState.capability). */
1681
+ capability?: Capability;
1682
+ }
1683
+ /**
1684
+ * 작업 하나의 관측 상태 — **ISA-95 `SegmentResponse`**(Part 4, 실적).
1685
+ *
1686
+ * 1차 출처(B2MML v0701): `SegmentResponseType`(B2MML-OperationsPerformance.xsd) — 실적 하나에 응답이
1687
+ * 여럿 들리고, 응답마다 어느 공정 구간을 언제 누구·무엇으로 했는지가 실린다(`PersonnelActual` ·
1688
+ * `EquipmentActual` · `MaterialActual`). 이 계약의 `personnel`·`resourceRef`·`materialActual` 이 그
1689
+ * 자리들이다.
1690
+ */
1691
+ /**
1692
+ * **작업 지시서 참조**(ISA-95 `WorkDirective`) — 어느 판의 절차로 만들었나.
1693
+ *
1694
+ * ── 내용은 담지 않는다 (2026-08-30) ───────────────────────────────────────
1695
+ * 절차·도면·설정값은 MES 가 갖는다. 트윈은 그것으로 계산하지 않는다.
1696
+ *
1697
+ * 담는 것은 **정체성과 판**이다. 같은 작업지시라도 절차 3판으로 만든 것과 4판으로 만든 것은 다른
1698
+ * 사실이고, 나중에 문제가 났을 때 어느 판이었는지 말할 수 없으면 트윈이 답을 못 한다. 표준이
1699
+ * `WorkDirective` 를 판으로 관리하는 이유가 그것이다.
1700
+ *
1701
+ * `WorkMaster` 는 「이 제품을 이렇게 만든다」이고 이것은 「이번에 이 절차로 만들었다」이다.
1702
+ */
1703
+ export interface WorkDirectiveRef {
1704
+ id: string;
1705
+ /** 절차의 판 — 없으면 「판을 말하지 않은 원천」이고, 지어내지 않는다. */
1706
+ version?: string;
1707
+ /** 이 지시서가 따르는 `WorkMaster` — 있을 때만. */
1708
+ workMasterId?: string;
1709
+ }
1710
+ export interface TaskState {
1711
+ id: string;
1712
+ kind: string;
1713
+ status: TaskStatus;
1714
+ /** 어느 판의 절차로 하는 작업인가 — 계보의 사실이다(§`WorkDirectiveRef`). */
1715
+ workDirective?: WorkDirectiveRef;
1716
+ itemRefs?: string[];
1717
+ fromNode?: string;
1718
+ toNode?: string;
1719
+ resourceRef?: string;
1720
+ orderId?: string;
1721
+ progress?: number;
1722
+ /**
1723
+ * 남은 시간·총 소요(ms) — 진행 중인 작업을 이어서 실행하는 데 필요(씨앗의 충실도).
1724
+ * `remainingMs` 는 **마지막 전이 시점의 값**이다. 델타는 매 tick 오지 않으므로(설계) 미러가 든 값은
1725
+ * 그때의 것이고, 지금 값은 `startedAtSimMs` 로 보간한다 — 모션(EquipmentMotion)과 같은 규율.
1726
+ */
1727
+ remainingMs?: number;
1728
+ durationMs?: number;
1729
+ /**
1730
+ * 착수 시각(절대 sim-clock) — **보간 앵커.** 이것이 없으면 미러는 "그때 얼마 남았었나" 만 알고
1731
+ * "지금 얼마 남았나" 를 못 낸다. progress = (now − startedAtSimMs) / durationMs.
1732
+ */
1733
+ startedAtSimMs?: number;
1734
+ /** 작업 의도 — 무자원이 설계인지(체류) 기록 누락인지 구별하는 근거. */
1735
+ intent?: 'transport' | 'process' | 'dwell';
1736
+ /**
1737
+ * 이 작업에 투입된 사람들 — 설비(`resourceRef`)와 **별개 축**이다.
1738
+ * 한 작업이 설비 하나와 사람 여럿을 동시에 잡을 수 있다(용접 로봇 1대 + 작업자 2명).
1739
+ */
1740
+ personnel?: string[];
1741
+ /**
1742
+ * 우선순위 — 표준 `JobOrder.Priority`. **작은 값이 급하다**(§PRIORITY_UNSET). 없으면 우선순위 없음.
1743
+ *
1744
+ * 없을 때는 선착순만 가능했다 — 급한 일을 앞세우는 것이 운영의 기본인데 모델에 자리가 없었다.
1745
+ */
1746
+ priority?: number;
1747
+ /** 예정 착수 — 표준 `JobOrder.StartTime`. 실제 착수(`startedAtSimMs`)와 다른 축이다. */
1748
+ startTime?: ISOTime;
1749
+ /** 예정 완료(납기) — 표준 `JobOrder.EndTime`. 없으면 **지연을 정의할 수 없다**(§dueStatusOf). */
1750
+ endTime?: ISOTime;
1751
+ /**
1752
+ * **실제로 들어가고 나온 자재** — ISA-95 `JobResponse.MaterialActual`(`OpMaterialActualType`).
1753
+ * 명세(계획)가 아니라 일어난 일이다. 투입 인원·설비와 같은 채널에 실어 실적을 한 곳에서 읽는다.
1754
+ */
1755
+ materialActual?: {
1756
+ definitionId: string;
1757
+ use: 'consumed' | 'produced';
1758
+ quantity: number;
1759
+ uom?: string;
1760
+ }[];
1761
+ }
1762
+ /**
1763
+ * 오더 하나의 관측 상태 — **ISA-95 `OperationsRequest`**(Part 4, 일정).
1764
+ *
1765
+ * 1차 출처(B2MML v0701): `OperationsRequestType`(B2MML-OperationsSchedule.xsd) — 일정 하나에 요구가
1766
+ * 여럿 들리고, 요구는 `SegmentRequirement` 로 쪼개진다. 여기 담는 것은 **원본이 낸 요구를 트윈이
1767
+ * 관측한 것**이라 계획 자체가 아니다(그 구분이 ADR-0030 의 이유다).
1768
+ *
1769
+ * 실적 쪽 대응은 `TaskState`(`SegmentResponse`) 다 — 요구와 응답을 한 짝으로 읽어야 "얼마나 시켰고
1770
+ * 얼마나 됐나" 가 나온다.
1771
+ */
1772
+ export interface OrderState {
1773
+ id: string;
1774
+ kind: string;
1775
+ status: string;
1776
+ /**
1777
+ * **무엇을 만드나** — GS1 품목 참조(단일 품목 작업지시). 여러 품목이면 `lines[].gtin` 이 말한다.
1778
+ *
1779
+ * 이 자리가 없던 동안 상태에는 "1개" 만 남고 **무엇을 1개인지** 사라졌다. 화면·예측·계보가 품목을
1780
+ * 알 수 없다는 뜻이고, 확보분(`allocated`)이 같은 식으로 빠져 계보의 절반이 사라졌던 것과 같은 부류다.
1781
+ */
1782
+ gtin?: string;
1783
+ /**
1784
+ * **무엇으로 만드나** — 이 오더가 든 레시피(표준 `OperationsRequest` → `SegmentRequirement`).
1785
+ *
1786
+ * 품목만으로는 「무엇으로」가 남지 않는다: 같은 품목에 대체 레시피가 있을 수 있고, 그러면 소요·라우트·
1787
+ * 소요시간이 다르다. 없으면 소비처는 **묻지 않는다**(레시피가 하나인 트윈이다).
1788
+ */
1789
+ recipeKey?: string;
1790
+ /** 씨앗이 이 오더의 확보분을 다 심지 못했다 — 이 오더의 답은 부족한 씨앗 위에 있다. */
1791
+ seedIncomplete?: boolean;
1792
+ /**
1793
+ * **이 오더가 왜 대기하는가** — 모자란 투입 줄(자재 키 · 필요 · 확보 가능).
1794
+ *
1795
+ * 확보는 전량 아니면 대기다. 그 거동은 옳은데 이유를 말하지 않으면 화면은 「running」만 보여 주고,
1796
+ * 사람은 영구히 대기하는 오더를 정상으로 읽는다. 모자란 줄을 **전부** 싣는다 — 하나만 알려 주면
1797
+ * 채운 뒤 다음 줄에서 또 막히고 같은 진단을 반복하게 된다.
1798
+ */
1799
+ shortage?: {
1800
+ material: string;
1801
+ need: number;
1802
+ have: number;
1803
+ }[];
1804
+ progress?: number;
1805
+ held?: boolean;
1806
+ /**
1807
+ * 요청·이행 수량과 라인 — **진척(progress)으로 압축하지 않는다.**
1808
+ *
1809
+ * 예전에는 상태가 `progress` 하나만 들고 있어서, 미러에서 예측을 세우려면 **저널을 따로 읽어**
1810
+ * 오더 원값을 되찾아야 했다(`buildForecastKernel`). 남은 데맨드(라인별 requested − fulfilled)를
1811
+ * 모르면 "무엇을 얼마나 더 내보내야 하나" 를 재계획할 수 없기 때문이다.
1812
+ *
1813
+ * 진척은 이 둘에서 나오는 파생값이다 — 파생을 남기고 원본을 버린 것이 잘못이었다.
1814
+ */
1815
+ requested?: number;
1816
+ fulfilled?: number;
1817
+ lines?: ObservedOrderLine[];
1818
+ /**
1819
+ * 이 오더가 **확보해 둔 물품들** — 진행 중인 할당.
1820
+ *
1821
+ * 이것이 상태에 없으면 **웜스타트가 잃는다.** 잃으면 되살아난 오더는 아무것도 안 잡은 것처럼
1822
+ * 보이고, 그 오더에 딸린 진행 중 작업이 완료될 때 계보(`TransformationEvent`)가 **입력 없이**
1823
+ * 나간다 — "무엇이 무엇으로 바뀌었나" 의 절반이 사라진다. 실제로 그렇게 되고 있었다.
1824
+ *
1825
+ * 씨앗이 잃은 것은 예측도 모른다 — 진행 중 자재를 안 잡은 것으로 놓고 미래를 실행하면 답이
1826
+ * 낙관 쪽으로 치우친다(`hydrateObserved` 가 상태를 지키는 이유와 같다).
1827
+ */
1828
+ allocated?: string[];
1829
+ /**
1830
+ * 이 오더의 거래번호(작업지시·PO·SO) — 계보와 EPCIS 이벤트가 **무슨 거래로** 일어났는지 잇는 축.
1831
+ * 이것도 상태에 없어서 되살아난 오더는 빈 문자열을 갖고, 이후 모든 이벤트의 거래번호가 빈다.
1832
+ */
1833
+ bizTransaction?: string;
1834
+ /**
1835
+ * 이 오더가 **이행하는 계획** — 표준 `OperationsRequestID`.
1836
+ *
1837
+ * ── 왜 부모-자식이 아닌가 (원문 대조 2026-08-24) ───────────────────────────
1838
+ * 「생산 계획 하나에 작업지시 여럿」을 담아야 했고, `parentOrderId` 같은 위계를 만들려다 원문을
1839
+ * 읽었다. **`OperationsRequestType` 에는 부모 축이 없다** — 계획은 `SegmentRequirement` 로 분해되고
1840
+ * 자식 계획을 갖지 않는다.
1841
+ *
1842
+ * 표준이 쓰는 방향은 반대다: **이행하는 쪽이 계획을 가리킨다.** `OperationsResponseType` 과
1843
+ * `OpSegmentResponseType` **둘 다** `OperationsRequestID` 를 든다(B2MML-OperationsPerformance ·
1844
+ * OperationsPerformanceTypes). 그래서 위계를 발명하지 않고 그 참조를 그대로 쓴다.
1845
+ *
1846
+ * 그러면 「이 계획이 얼마나 이행됐나」는 이 값으로 묶어 답한다 — **새 축을 만들지 않는다.** 계획
1847
+ * 자체도 오더로 들어오면 그 오더의 `requested` 가 계획량이고, 묶인 것들의 합과 **다를 수 있다.**
1848
+ * 그 차이가 곧 진단이므로 둘을 같은 값으로 만들지 않는다.
1849
+ *
1850
+ * `bizTransaction` 과 다르다 — 그것은 외부 거래(PO/SO)이고 이것은 **우리 안의 상위 계획**이다.
1851
+ */
1852
+ operationsRequestId?: string;
1853
+ /**
1854
+ * 도메인이 **약속해 둔 자리와 시각창** — 어디로/언제 들이기로 했는가.
1855
+ *
1856
+ * ── 왜 사실에 실어야 하나 (2026-08-17) ─────────────────────────────────────
1857
+ * 야드 트윈이 재기동 뒤 **첫 틱에서 죽었다.** 어포인트먼트를 받을 때 도크 도어를 정해 두는데
1858
+ * 그것이 어느 사실에도 실리지 않아, 웜스타트가 오더는 되살리고 배정은 잃었다. 그리고 다음 틱에
1859
+ * 「그 도어의 점유」를 읽다 예외가 났다(`Cannot read properties of undefined`). 안전망이 그 트윈만
1860
+ * 세워 다른 트윈은 살았지만, 야드 트윈 셋이 부팅마다 죽는 상태였다.
1861
+ *
1862
+ * `allocated` 가 두 자리에만 있어 계보를 잃었던 것과 **같은 부류**다: 상태가 필요로 하는 것을
1863
+ * 사실이 갖고 있지 않으면 재기동을 넘지 못한다.
1864
+ */
1865
+ dockDoor?: string;
1866
+ windowStartMs?: number;
1867
+ /** 우선순위 — 표준 `OperationsRequest.Priority`. 작은 값이 급하다. 오더 할당 순서를 정한다. */
1868
+ priority?: number;
1869
+ /** 예정 착수 — 표준 `OperationsRequest.StartTime`. */
1870
+ startTime?: ISOTime;
1871
+ /** 납기 — 표준 `OperationsRequest.EndTime`. 이것이 있어야 "늦었나" 를 물을 수 있다. */
1872
+ endTime?: ISOTime;
1873
+ }
1874
+ export type AttentionSeverity = 'low' | 'medium' | 'high' | 'critical';
1875
+ export type AttentionState = 'active' | 'acknowledged' | 'cleared';
1876
+ export interface Attention {
1877
+ id: string;
1878
+ kind: string;
1879
+ severity: AttentionSeverity;
1880
+ state?: AttentionState;
1881
+ /**
1882
+ * 이 신호가 가리키는 대상 — 공간 위 위치로 해석한다.
1883
+ *
1884
+ * `operation` 은 개체가 아니라 **공정(단계)** 이다. 개체 셋(자리·설비·오더)만으로는 "도장이라는
1885
+ * 단계가 라인의 제약이다" 를 가리킬 수 없다 — 도장 부스는 여럿이고 그중 하나가 문제인 게 아니라
1886
+ * **그 단계 전체**가 모자란 것이다. 표준에도 자리가 있다(ISA-95 `OperationsSegment`).
1887
+ */
1888
+ anchor: {
1889
+ locationId?: string;
1890
+ moverId?: string;
1891
+ orderId?: string;
1892
+ operation?: string;
1893
+ };
1894
+ /**
1895
+ * **이 조건이 언제부터인가** — 표준 `WorkAlertType.TimeStamp`(ISO 절대 시각).
1896
+ *
1897
+ * ── 죽은 필드였다 ────────────────────────────────────────────────────────
1898
+ * 예전 이름은 `since`(simClockMs)였는데 **한 번도 채워지지 않았고 아무도 읽지 않았다.** 그래서
1899
+ * 사용자는 병목이 **10초째인지 3시간째인지** 구별할 수 없었다 — 방금 찬 자리와 세 시간째 막힌 자리는
1900
+ * 할 일이 완전히 다르다. 표준이 알림에 시각을 두는 이유가 그것이다.
1901
+ *
1902
+ * **재평가 시각이 아니라 조건이 처음 성립한 시각**이다. 주목 신호는 매 스냅샷 다시 계산되므로,
1903
+ * "지금" 을 찍으면 언제나 방금 생긴 것처럼 보인다(그 값은 스냅샷 시각과 같아 아무 정보가 없다).
1904
+ * 그래서 커널이 **처음 본 시각을 기억**한다(조건이 사라지면 함께 지운다 — 확인(ack)과 같은 수명).
1905
+ *
1906
+ * 시뮬 클록(ms)이 아니라 **절대 시각**이다: 소비처가 화면에 "3시간째" 를 쓰려면 기준이 필요하고,
1907
+ * 라이브에서는 시뮬 클록이 아예 뜻이 없다.
1908
+ */
1909
+ timeStamp?: ISOTime;
1910
+ /**
1911
+ * 표현용 원시 파라미터(언어 중립). 커널은 사람이 읽는 문장을 만들지 않는다 — kind + params 만 방출하고
1912
+ * title/detail/rationale 는 표현계층(클라 i18next 템플릿)이 kind 로 키를 골라 params 를 보간해 렌더.
1913
+ * 예: bottleneck → { locationId, occupancy, capacity, ratioPct, saturated }.
1914
+ */
1915
+ params?: Record<string, string | number>;
1916
+ recommendedActions?: RecommendedAction[];
1917
+ suggestedAction?: {
1918
+ code: string;
1919
+ command: string;
1920
+ args?: unknown;
1921
+ };
1922
+ }
1923
+ /** 계량 지점의 마지막 관측 — 값이 없으면 **모르는 것이다**(0 이 아니다). */
1924
+ export interface MeterPointState {
1925
+ id: string;
1926
+ /**
1927
+ * 이 계량 지점이 **모델에 선언된 것인가, 계측이 처음 데려온 것인가** (자리와 같은 어휘).
1928
+ *
1929
+ * ── 왜 표시해야 하나 ───────────────────────────────────────────────────────
1930
+ * 계측은 모르는 `meterId` 를 만나면 **조용히 새 지점을 만든다.** 그 kW 는 그 현장의 수요·피크에
1931
+ * 그대로 더해지므로(그것이 옳다 — 빼면 「계약 안쪽」이라는 더 위험한 거짓이 된다), 표시가 없으면
1932
+ * 커넥터 매핑에서 id 를 한 글자 틀린 것과 정상을 구별할 수 없다. 그때 진짜 계량기는 조용히 멈추고
1933
+ * 유령이 누적되며, **숫자는 그럴듯하고 틀린다.** 요금 판정까지 가는 축이라 자리 오타보다 무겁다.
1934
+ *
1935
+ * `'master'` = 모델(설비 선언)에 있는 계량기. `'observed'` = 계측만 데려온 계량기.
1936
+ * 옛 스냅샷에는 이 칸이 없다(`undefined`) — 「선언되었다」로 읽지 말 것. 선언 여부의 정본은 모델이고,
1937
+ * 커널은 표본이 올 때마다 이 값을 다시 맞춘다.
1938
+ */
1939
+ origin?: 'master' | 'observed';
1940
+ /**
1941
+ * **무엇을 재는 계량기인가** — 쓰는 쪽(`import`) · 내는 쪽(`export`) · 양쪽(`bidirectional`).
1942
+ *
1943
+ * 관측이 아니라 **선언**에서 온다(자원 속성 `meter.direction`). 표본마다 바뀌는 값이 아니고, 그
1944
+ * 계량기가 무엇을 재도록 설치되었는지는 현장이 안다. 그래서 표본에는 이 칸이 없다.
1945
+ *
1946
+ * 선언하지 않으면 이 칸이 없고, 그때 그 지점은 **부하로 센다**. 빼면 부하가 오류 없이 작아지고, 작아진
1947
+ * 부하는 「계약 안쪽」이라는 더 위험한 거짓을 만든다.
1948
+ */
1949
+ direction?: 'import' | 'export' | 'bidirectional';
1950
+ kW?: number;
1951
+ /** 누적 전력량(원천이 준 적산값) — 우리가 적분한 값이 아니다. */
1952
+ kWh?: number;
1953
+ powerFactor?: number;
1954
+ /** 마지막 표본의 시각 — 계측이 끊긴 것을 소비처가 알 수 있게. */
1955
+ atMs?: number;
1956
+ /** 이 구간에 받은 표본 수 — 「못 쟀다」와 「0이었다」를 구별한다. */
1957
+ samplesInWindow: number;
1958
+ /**
1959
+ * 이 구간이 열릴 때의 적산값 — **구간 전력량을 뺄셈으로 얻기 위한 기준점.**
1960
+ *
1961
+ * 지점별 구간 전력량은 「끝 적산 − 시작 적산」이다. 그것을 우리가 kW 로 적분하지 않는 이유는,
1962
+ * 적산은 **계량기 자신의 회계**이고 그것이 요금의 근거이기 때문이다(우리가 적분한 값과 미세하게
1963
+ * 다르며, 다를 때 맞는 쪽은 계량기다).
1964
+ */
1965
+ kWhAtWindowStart?: number;
1966
+ /**
1967
+ * 밖에서 마감되어 온 **마지막 사용 구간** — 계량 표본이 없는 현장의 화면이 「지난 구간에 얼마 썼나」를
1968
+ * 말할 수 있게(§`ENERGY_EVENT.usagePeriod`).
1969
+ *
1970
+ * 우리가 마감한 구간(`kWhAtWindowStart` 로 뺀 값)과 **다른 값이다.** 더하지 않는다.
1971
+ */
1972
+ lastPeriodKWh?: number;
1973
+ lastPeriodFrom?: ISOTime;
1974
+ lastPeriodTo?: ISOTime;
1975
+ /** 그 값의 근거 — 계량인지, 공급자가 준 것인지, 청구서인지. */
1976
+ lastPeriodBasis?: ObservationBasis;
1977
+ }
1978
+ export interface DemandWindowState {
1979
+ startMs: number;
1980
+ endMs: number;
1981
+ /**
1982
+ * `maxKW` 가 찍힌 **그 순간**의, **모델이 선언한** 뿌리 계량기들만의 합.
1983
+ *
1984
+ * `maxKW` 는 모르는 계량기까지 포함한 합이다(그것을 빼면 부하가 오류 없이 작아지고, 작아진 부하는
1985
+ * 「계약 안쪽」이라는 더 위험한 거짓을 만든다). 그러나 그 값 하나만 내면 반대쪽 거짓이 생긴다 —
1986
+ * 오타로 생긴 유령 계량기가 계약 초과를 만들어도 화면은 그것을 진짜 초과로 말한다.
1987
+ *
1988
+ * 그래서 **두 벌을 낸다.** 화면은 「선언된 것으로는 82%, 정체 모를 계량기까지 넣으면 104%」라고
1989
+ * 말할 수 있다 — 빼서 정확한 척하지 않고, 더해서 뭉개지도 않는다. 두 값이 같으면 모든 뿌리가
1990
+ * 선언된 것이고, 그것도 사실이다. 잰 적이 없으면 `undefined` 다(0 이 아니다).
1991
+ */
1992
+ maxDeclaredKW?: number;
1993
+ /**
1994
+ * 이 구간의 **최대가 찍힌 순간**의 지점별 kW — 요금이 걸린 수는 kW 다.
1995
+ *
1996
+ * 나중에 계산할 수 없다: 커널은 표본을 보관하지 않으므로 그 순간이 지나면 각 지점이 얼마였는지 알 길이
1997
+ * 없다. 그래서 최대가 갱신되는 순간에 찍어 둔다. 「어느 분기를 깎아야 피크가 내려가나」의 유일한 근거다
1998
+ * (전력량이 큰 분기와 피크를 만든 분기는 다를 수 있다).
1999
+ */
2000
+ pointsAtPeak?: {
2001
+ id: string;
2002
+ kW: number;
2003
+ }[];
2004
+ /**
2005
+ * 그 구간의 최대 순간부하 — 표본이 없으면 `undefined`(0 이 아니다).
2006
+ *
2007
+ * **계통에서 끌어온 수요**다(요금이 이것에 매겨진다). 현장에 발전·축전이 있으면 설비가 끌어당긴
2008
+ * 양과 다르다 — 그 차이는 `grossMaxKW` 와의 간격으로 읽는다.
2009
+ */
2010
+ maxKW?: number;
2011
+ /**
2012
+ * 설비가 **끌어당긴** 최대(발전·방전 차감 전, 충전 포함).
2013
+ *
2014
+ * ── 왜 둘을 함께 두나 (2026-08-17) ────────────────────────────────────────
2015
+ * 요금은 계통 수요(`maxKW`)로 매겨지지만, 「무엇이 그 수요를 만들었나」는 설비 부하가 답한다.
2016
+ * 하나만 두면 둘 중 하나를 잃는다: 순수요만 두면 태양광이 잠깐 가려졌을 때 왜 피크가 올랐는지 설명할
2017
+ * 수 없고, 총부하만 두면 요금이 실제보다 커 보인다.
2018
+ *
2019
+ * 그리고 **이 둘의 간격이 곧 깎인 양**이다(피크 셰이빙). 같은 표본에서 나온 두 최대값이므로 비교가
2020
+ * 성립한다 — 서로 다른 창의 값을 견주는 것이 아니다.
2021
+ *
2022
+ * 발전·축전을 선언하지 않은 현장에서는 `maxKW` 와 같다(그때는 차감할 것이 없다).
2023
+ */
2024
+ grossMaxKW?: number;
2025
+ /**
2026
+ * 이 구간에 **내보낸 최대**(kW) — 내는 쪽으로 선언된 계량 지점들의 합.
2027
+ *
2028
+ * 수요의 최대와 **같은 순간이 아니다.** 해가 가장 좋은 때와 부하가 가장 큰 때는 다르고, 한쪽 순간에
2029
+ * 맞춰 다른 쪽을 적으면 두 수 다 사실이 아니게 된다.
2030
+ *
2031
+ * ── 왜 빼지 않고 따로 내나 (2026-08-25) ───────────────────────────────────
2032
+ * 내보낸 양을 재는 계량은 수요에 **더하지 않는다**(그것이 이 축을 만든 이유다). 그렇다고 수요에서 **빼지도
2033
+ * 않는다**: 수전 계량기가 이미 상계 계량이면 그 값에 발전이 반영되어 있고, 거기서 또 빼면 두 번
2034
+ * 깎는다. 어느 쪽인지는 현장의 결선이 정하는 것이고 우리가 표본만 보고 알 수는 없다.
2035
+ *
2036
+ * 그래서 두 수를 나란히 낸다. 화면은 「계통에서 받은 최대 320kW, 같은 순간 내보낸 것 180kW」라고
2037
+ * 말할 수 있다. 자체 구동 경로는 설비의 발전량으로 순수요를 계산한다(§`grossMaxKW`) — 그것은 우리가
2038
+ * 만든 값이라 이중 계상이 없다.
2039
+ *
2040
+ * 잰 적이 없으면 `undefined` 다(0 이 아니다).
2041
+ */
2042
+ exportKW?: number;
2043
+ /** 표본 평균 부하 — 요금 산정의 평균 수요에 대응한다. */
2044
+ meanKW?: number;
2045
+ /** **kW 를 실은** 표본 수 — 부하 판정의 근거 수다(받은 표본 전체가 아니다). */
2046
+ samples: number;
2047
+ /** 계약전력 대비 — 계약을 모르면 `undefined`(짐작하지 않는다). */
2048
+ contractKW?: number;
2049
+ overContract?: boolean;
2050
+ /**
2051
+ * 이 구간의 값이 **잰 것이 아니라 만든 것**임을 밝힌다(시뮬레이션).
2052
+ *
2053
+ * 시뮬 트윈은 선언된 부하 계수와 설비 가동 상태로 부하를 계산한다. 그 수를 계측과 같은 자리에
2054
+ * 두면 화면·성과·보고서가 그것을 실측으로 읽는다 — 그래서 값 옆에 종류를 함께 싣는다.
2055
+ * 미러 트윈에서는 절대 켜지지 않는다(관측 구동은 잰 것만 쓴다).
2056
+ */
2057
+ derived?: boolean;
2058
+ }
2059
+ export interface EnergyState {
2060
+ points: MeterPointState[];
2061
+ /** 지금 열려 있는 구간 — 아직 마감되지 않았다(예측은 `projectedKW`). */
2062
+ open?: DemandWindowState & {
2063
+ projectedKW?: number;
2064
+ projectionBasis?: 'mean-so-far';
2065
+ };
2066
+ /**
2067
+ * 마감된 구간들 — **최근 것만 들고 있다**(상태 크기가 시간에 비례하지 않게).
2068
+ * 자른 사실을 `closedTotal` 로 함께 낸다 — 조용히 자르지 않는다.
2069
+ */
2070
+ closed: DemandWindowState[];
2071
+ closedTotal: number;
2072
+ /** 관측 시작 이후 최대 수요 — 월 경계는 여기서 정하지 않는다(위 주석). */
2073
+ peakSince?: {
2074
+ kW: number;
2075
+ windowStartMs: number;
2076
+ };
2077
+ /**
2078
+ * 우리 모델이 모르는 설비가 상태를 보내 온 횟수 — **버린 것을 세어 둔다.**
2079
+ *
2080
+ * 원천에 우리가 모르는 설비가 있다는 것은 그 자체로 알아야 할 사실이다(모델이 낡았거나 매핑이
2081
+ * 틀렸다). 조용히 버리면 「값이 왜 안 보이지」로만 남는다.
2082
+ *
2083
+ * **자리를 못 박아 둔다**: 이 칸은 `state.energy` 의 것이다 — 스냅샷 루트가 아니다. 예전에는 이 선언이
2084
+ * `peakSince` 의 타입 안에 갇혀 있었다(중괄호 하나가 닫히지 않았다). 타입은 통과했지만 계약이 사실과
2085
+ * 달라서, 읽는 쪽이 자리를 짐작하다 스냅샷 루트에서 읽고 **영원히 `null`** 을 받았다 — 오류 없이
2086
+ * 「에너지 트윈이 아니다」로 읽히는 종류의 거짓이다.
2087
+ *
2088
+ * 0 이면 이 칸을 만들지 않는다. 「세었고 0」과 「에너지 트윈이 아님」은 `energy` 자체가 있는지로 가른다.
2089
+ */
2090
+ unknownEquipment?: number;
2091
+ /** 현장이 선언한 계약전력(자리 속성) — 없으면 계약 대비 판정을 하지 않는다. */
2092
+ contractKW?: number;
2093
+ /**
2094
+ * 이 커널이 받은 **물류 흐름 요청** — 에너지에는 없는 것들이다(도착·오더·배정·작업 완료).
2095
+ * 비어 있지 않으면 배선 오류다: EMS 트윈에 물류 명령이 오고 있다. 조용히 넘기지 않는다.
2096
+ */
2097
+ flowRequests?: {
2098
+ hook: string;
2099
+ count: number;
2100
+ }[];
2101
+ /**
2102
+ * 마지막으로 받은 **청구서** — 공급자가 확정한 금액(§`ENERGY_EVENT.bill`).
2103
+ *
2104
+ * 우리가 계산한 금액을 덮지 않는다. 읽는 쪽이 둘을 견주어 어긋나면 그 사실을 낸다 — 우리 계산이
2105
+ * 틀렸다는 뜻이고, 사람이 찾아내지 않아도 드러나야 한다.
2106
+ */
2107
+ lastBill?: EnergyBillData;
2108
+ /**
2109
+ * 마지막으로 받은 **요금 기준** — 이 주기에 적용되는 요금적용전력과 단가.
2110
+ *
2111
+ * 청구서보다 이쪽이 먼저다: 청구서는 끝난 기간의 정산이고 이것은 지금 적용되는 값이다.
2112
+ */
2113
+ tariffBasis?: EnergyTariffBasisData;
2114
+ /**
2115
+ * **선언된 요금 기준들** — 유효 구간별. `tariffBasis` 는 이 중 지금 유효한 것이다.
2116
+ *
2117
+ * ── 왜 여럿을 드나 (2026-08-30) ──────────────────────────────────────────
2118
+ * 선언은 **예정될 수 있다.** 한전은 다음 달 요금을 미리 공표한다. 하나만 들면 9월 선언이 들어오는
2119
+ * 순간 8월 선언을 덮어써서 **오늘 요금이 다음 달 단가로 계산된다.** 오류는 나지 않는다.
2120
+ *
2121
+ * 처음에는 예정된 선언을 유입에서 거부해 이 상황을 막았다. 그것은 정상적인 사실을 막는 것이라,
2122
+ * 담을 자리를 만들고 거부를 풀었다.
2123
+ *
2124
+ * 고르는 규칙은 **커널에 한 벌만 둔다.** 읽는 쪽이 각자 고르면 화면마다 다른 요금이 나온다.
2125
+ */
2126
+ tariffBasisSchedule?: EnergyTariffBasisData[];
2127
+ /**
2128
+ * 이 트윈의 **발전 정격**과 그 근거 — 이용률의 분모(§ `generationRated`).
2129
+ *
2130
+ * 선언에서 고른 값이라 상태에 낸다. 읽는 쪽이 모델을 다시 뒤지면 고르는 규칙이 두 벌이 되고,
2131
+ * 그때부터 두 화면이 다른 이용률을 말한다.
2132
+ */
2133
+ generationRated?: {
2134
+ kW: number;
2135
+ basis: 'ac' | 'dc';
2136
+ from: 'equipment' | 'location';
2137
+ };
2138
+ /**
2139
+ * 마지막으로 받은 **발전 단가** — 이 기간에 낸 전기 1kWh 의 값.
2140
+ *
2141
+ * 창 밖의 단가도 이 창에 적용될 수 있어(단가는 기간마다 정해진다) 상태로 이어 간다.
2142
+ *
2143
+ * 예정된 단가는 `generationPriceSchedule` 에 함께 든다 — 이 칸은 **지금 유효한 것**이다.
2144
+ */
2145
+ generationPrice?: EnergyGenerationPriceData;
2146
+ /** **선언된 발전 단가들** — 유효 구간별. 예정된 것을 담는다(§`tariffBasisSchedule`). */
2147
+ generationPriceSchedule?: EnergyGenerationPriceData[];
2148
+ /**
2149
+ * 이 청구 주기의 **최고 수요** — 기본요금이 이 값으로 다시 정해진다.
2150
+ *
2151
+ * `periodStart` 는 마지막 청구서의 끝이다(그 뒤가 아직 청구되지 않은 구간). 그것이 바뀌면 처음부터
2152
+ * 다시 센다 — 지난 주기의 최고가 이번 주기를 정하지 않는다.
2153
+ */
2154
+ demandHigh?: {
2155
+ kW: number;
2156
+ atTo: ISOTime;
2157
+ periodStart?: ISOTime;
2158
+ since: ISOTime;
2159
+ };
2160
+ /**
2161
+ * 설비마다 **지금 기간의 발전 관측** — 기간 발전량을 확정할 때 쓰는 기준점.
2162
+ *
2163
+ * 계량 지점이 `kWhAtWindowStart` 를 상태에 두는 것과 같은 이유다: 재기동에서 이것이 없으면 그날의
2164
+ * 발전량이 부팅 이후로만 잡히고, **적게 잡힌 것이 드러나지 않는다.**
2165
+ */
2166
+ generationPeriods?: GenerationPeriodObservation[];
2167
+ /**
2168
+ * **마감하지 못한 기간 누적**의 수 — 월·요금기간 종류인데 원본이 기준점(`kWhSince`)을 말하지 않으면
2169
+ * 커널은 그 기간이 언제 끝나는지 알 수 없다(자정으로는 끝나지 않는다).
2170
+ *
2171
+ * 세어 두는 이유: 세지 않으면 그 발전소의 발전량이 아무 표시 없이 영원히 나오지 않는다. 0 이면 이
2172
+ * 칸을 만들지 않는다.
2173
+ */
2174
+ unclosedGenerationPeriods?: number;
2175
+ }
2176
+ /** 지금 기간의 발전 관측 — 기간 발전량은 이 기준점과 마지막 관측의 관계로 정해진다. */
2177
+ export interface GenerationPeriodObservation {
2178
+ equipmentId: string;
2179
+ /** 선언된 시각 기준의 며칠째인가(§`localDayIndexAt`). */
2180
+ dayIndex: number;
2181
+ /** 이 기간에서 처음 관측한 시각 — 기간 시작과 다르면 그만큼 재지 않았다. */
2182
+ fromMs: number;
2183
+ /** 이 기간에서 마지막으로 본 적산과 그 시각 — 기간 누적은 이것이 곧 총량이다. */
2184
+ lastKWh: number;
2185
+ lastMs: number;
2186
+ /**
2187
+ * 그 적산이 **어떤 누적인가** — 마감할 때 총량을 얻는 방법이 이것으로 갈린다.
2188
+ *
2189
+ * ── 왜 여기 함께 두나 (2026-08-30) ────────────────────────────────────────
2190
+ * 설비 상태는 원천이 다시 보내 주므로 재기동에서 되살리지 않는다(관측 축은 원천에서). 그 규율은
2191
+ * 맞다. 그런데 **누적의 종류는 관측이 아니라 그 원본이 어떤 값을 주는지에 대한 사실**이다 —
2192
+ * 값은 다음 표본에 다시 오지만 종류도 그때까지 없다.
2193
+ *
2194
+ * 태양광은 밤에 표본을 보내지 않는다. 그래서 밤에 재기동하면 종류가 사라지고, 시계가 자정을
2195
+ * 지나도 **어느 기간의 값인지 몰라 마감하지 못한다.** 실제로 그렇게 됐다(2026-08-30 04:54).
2196
+ */
2197
+ accumulation?: 'lifetime' | 'daily' | 'monthly' | 'billing' | 'unknown';
2198
+ }
2199
+ /**
2200
+ * 조치방향 — code=안정 조치 키(언어 중립). command 있으면 원클릭 실행, 없으면 권고.
2201
+ * 라벨·힌트(사람 언어)는 표현계층이 code 로 렌더(커널은 문장 미보유). command 보유 조치는 code=command 문자열,
2202
+ * 권고만(hint only)이던 조치는 'advice.*' 코드.
2203
+ */
2204
+ export interface RecommendedAction {
2205
+ code: string;
2206
+ command?: string;
2207
+ args?: unknown;
2208
+ /**
2209
+ * 표현용 원시 파라미터 — 권고가 **무엇을 가리키는지** 말해야 할 때(예: 「어느 부하를 줄이나」).
2210
+ *
2211
+ * 실행 가능한 조치는 `args` 가 대상을 나른다(커맨드가 그것을 먹는다). 권고(command 없음)에는 그 자리가
2212
+ * 없어서, 예전에는 「부하를 줄이세요」까지만 말할 수 있었다 — 감축 가능 설비가 여럿이면 사람이 어느
2213
+ * 것인지 알 길이 없다. `Attention.params` 와 같은 규약이다(언어중립 원시값만, 문장은 표현계층이 만든다).
2214
+ */
2215
+ params?: Record<string, string | number>;
2216
+ }
2217
+ export interface StateSnapshot {
2218
+ revision: number;
2219
+ simClockMs: number;
2220
+ /**
2221
+ * **이 트윈의 "지금"**(ISO 절대 시각) — 시각으로 재는 모든 판단의 기준.
2222
+ *
2223
+ * `simClockMs` 로는 부족하다: 소비처가 절대 시각을 얻으려면 **기준 epoch 를 복제**해야 하고,
2224
+ * 관측(라이브) 모드에서는 그 계산이 아예 틀린다(그때의 "지금" 은 마지막으로 들은 발생 시각이다).
2225
+ * 트윈은 자기 시계로 사니, **그 시계를 트윈이 직접 말한다**.
2226
+ *
2227
+ * 없으면 소비처는 **재지 않는다** — 벽시계로 대신 재면 시뮬 트윈에서 엉뚱한 값이 나온다.
2228
+ */
2229
+ nowTime?: ISOTime;
2230
+ /**
2231
+ * **이 트윈의 정체성이 어디서 왔나** — 값이 아니라 **근거**다(§`identityGroundingOf`).
2232
+ *
2233
+ * 트윈당 한 번 싣는다. 사건마다 싣는 것은 비싸고 같은 사실의 반복이다. 없으면 소비처는 **판정하지
2234
+ * 않는다** — 기본값으로 `issued` 를 가정하면 그 화면이 곧 거짓이 된다.
2235
+ */
2236
+ identityGrounding?: IdentityGroundingView;
2237
+ /**
2238
+ * **원본과 어긋난 사실의 수** — 관측(미러) 구동에서만 생긴다.
2239
+ *
2240
+ * 미러는 원본을 비추는 쪽이라 어긋남을 만나도 멈추지 않는다. 그러면 그 사실이 사라지므로 여기 센다.
2241
+ * 없으면(0) 이 칸이 아예 없다 — 어긋난 적 없는 트윈에 빈 칸을 만들지 않는다.
2242
+ */
2243
+ conformance?: {
2244
+ /** 관측 구동에서 없는 입력을 만난 횟수 — 받아들였지만 사실이 맞지 않았다. */
2245
+ transformInputsAbsent?: number;
2246
+ /** 씨앗이 심지 못한 참조의 수 — 원본이 말했지만 그 물품이 스냅샷에 없었다. */
2247
+ seedDanglingRefs?: number;
2248
+ /**
2249
+ * 일반 요구(공정)와 구체 요구(레시피 × 공정)가 **등급 ↔ 품목으로 교차**한 횟수.
2250
+ *
2251
+ * 같은 키끼리는 구체가 상회한다. 교차는 뜻으로는 상회일 수 있으나 판정에 등급 소속이 필요하고,
2252
+ * 잘못 겹치면 자재가 조용히 사라지거나 두 배가 된다. 그래서 **둘 다 요구하고 센다** — 이 값이
2253
+ * 크면 그 숫자가 다음 작업을 정한다.
2254
+ */
2255
+ materialSpecCrossKeyOverlaps?: number;
2256
+ };
2257
+ locations: LocationState[];
2258
+ items: ItemState[];
2259
+ /**
2260
+ * 설비 — **ISA-95 `Equipment`.** 고정 설비(도장기·용접로봇)와 이동 설비(지게차·호슬러)를 한 그릇에
2261
+ * 든다. 둘의 상태 모델이 같기 때문이다(고장·가동·교대·계획정지·작업 점유). 갈리는 것은 하나뿐 —
2262
+ * 소속(`homeLocation`)과 현재 위치(`location`)가 같은지.
2263
+ *
2264
+ * 예전 이름은 `movers` 였다(씬의 움직임 믹스인에서 물려받은 것). 커널에는 애니메이션이 없고 (vocabulary-guard: allow — 개명 경위 서술)
2265
+ * 도장 부스는 아무것도 옮기지 않으므로 그 이름은 거짓이었다. 표준이 이미 정한 이름을 쓴다.
2266
+ */
2267
+ equipment: EquipmentState[];
2268
+ /** 사람 — 등급·교대·투입 상태. 인원을 선언하지 않은 트윈에서는 빈 배열. */
2269
+ persons: PersonState[];
2270
+ /** 물리 자산(반복사용) — 선언하지 않은 트윈에서는 빈 배열. */
2271
+ assets: AssetState[];
2272
+ tasks: TaskState[];
2273
+ orders: OrderState[];
2274
+ attentions?: Attention[];
2275
+ /**
2276
+ * **담을 줄 몰라 반영하지 못한 사실** — 종류별 수(관측 구동에서만 나온다).
2277
+ *
2278
+ * 관측 리듀서는 오래전부터 이것을 세어 왔다. 그런데 **스냅샷이 그 값을 떨어뜨렸다** — 그래서
2279
+ * 이미 그것을 읽도록 쓰여 있던 정합성 검사(`twin-unhandled-vocabulary`)는 미러에서 영원히
2280
+ * 조용했다(2026-08-19 실측). 세기만 하고 아무도 못 보는 값은 없는 것과 같다.
2281
+ */
2282
+ unhandled?: {
2283
+ eventType: string;
2284
+ count: number;
2285
+ firstAtMs?: number;
2286
+ lastAtMs?: number;
2287
+ }[];
2288
+ /**
2289
+ * **개체를 잇지 못한 채 다음 공정으로 넘어간 횟수** — 계보에 구멍이 남았다.
2290
+ *
2291
+ * ── 왜 이 값이 필요했나 (2026-08-24 실측) ────────────────────────────────────
2292
+ * 미러의 오더는 **확보분을 가진 적이 없다.** 원본이 「이 오더에 어느 개체가 잡혀 있나」를 말하지
2293
+ * 않는 것이 정상이다(첫 실 연동: 오더 사실이 `status`·`requested`·`fulfilled` 만 든다). 그런데
2294
+ * 커널이 중간 공정을 넘을 때 **들고 갈 개체를 요구하고 던졌다** — 그래서 미러에서 예측이
2295
+ * 구조적으로 실패했다(`twinForecast` 가 오더 하나 때문에 전부 실패).
2296
+ *
2297
+ * 진행을 막은 것이 과했다. **단계를 넘는 것은 공정의 진행이고 개체를 잇는 것은 계보**다. 계보를
2298
+ * 못 이으면 계보를 비우는 것이 맞고, 그렇다고 진행까지 멈추면 있는 답(언제 끝나나·처리량)까지 잃는다.
2299
+ *
2300
+ * 그래서 넘기되 **세어 낸다.** 세지 않으면 계보에 구멍이 있는데 화면이 정상으로 보인다 —
2301
+ * `unhandled` 를 스냅샷이 떨어뜨려 정합성 검사가 영원히 조용했던 것과 같은 부류의 실수다.
2302
+ *
2303
+ * **목록이 아니라 수다**: 오더 수가 규모에 비례해 자라므로 목록은 잘라야 하고, 자르면 「조용히
2304
+ * 자르지 않는다」를 지킬 수 없다. 어느 오더인지는 그 오더의 계보가 빈 것으로 답한다.
2305
+ *
2306
+ * 자체 구동(시뮬)에서는 **이 값이 오르지 않는다** — 시뮬은 개체를 스스로 만들므로 확보분이 비는
2307
+ * 것이 곧 결함이고, 거기서는 여전히 던진다(진짜 결함을 조용하게 만들지 않는다).
2308
+ */
2309
+ stepsWithoutMaterial?: number;
2310
+ /**
2311
+ * 확인(ack)해 둔 주목 신호 id — **상태에서 파생되지 않는 유일한 축.**
2312
+ *
2313
+ * 신호 자체는 상태에서 다시 계산되지만 "사람이 봤다" 는 계산으로 되살릴 수 없다. 스냅샷으로
2314
+ * 왕복시켜야 재기동·재계산에서 확인 상태가 유지된다.
2315
+ */
2316
+ acked?: string[];
2317
+ /**
2318
+ * 주목 신호가 **처음 성립한 시각**(id → ISO) — 확인과 마찬가지로 **계산으로 되살릴 수 없다.**
2319
+ *
2320
+ * 신호는 매 스냅샷 다시 계산되지만 「언제부터인가」는 그 계산 안에 없다. 왕복시키지 않으면 재기동·
2321
+ * 웜스타트 뒤 **세 시간째 지속된 조건이 「0초째」로 되살아난다** — 방금 찬 자리와 세 시간째 막힌
2322
+ * 자리는 할 일이 다르므로, 그 값이 거짓이면 화면은 사람을 잘못된 순서로 움직인다.
2323
+ *
2324
+ * 사라진 조건은 함께 지운다(재발은 새 시작이다) — 그 규율은 `computeAttentions` 가 지킨다.
2325
+ */
2326
+ attentionSince?: {
2327
+ id: string;
2328
+ since: ISOTime;
2329
+ }[];
2330
+ /**
2331
+ * 에너지 — **에너지 트윈만 채운다**(계량 지점·수요 구간·피크). 다른 종류에서는 없다.
2332
+ *
2333
+ * 없는 것과 빈 것을 구별한다: 필드가 아예 없으면 그 트윈은 에너지를 재지 않는 것이고,
2334
+ * `points: []` 는 「아직 표본이 없다」다.
2335
+ */
2336
+ energy?: EnergyState;
2337
+ }
2338
+ export interface Command<T = unknown> {
2339
+ commandId: string;
2340
+ type: string;
2341
+ tenantId: string;
2342
+ correlationId?: string;
2343
+ args: T;
2344
+ }
2345
+ export interface CommandAck {
2346
+ commandId: string;
2347
+ accepted: boolean;
2348
+ /**
2349
+ * 거절 사유 — 언어 중립. errorCode(안정 코드) + errorParams(원시값)로 방출하고 사람 언어는
2350
+ * 표현계층(클라 i18next)이 렌더한다(무방언·다국어, attention 과 동형). error 는 개발자/로그용 영어 폴백.
2351
+ */
2352
+ errorCode?: string;
2353
+ errorParams?: Record<string, string | number>;
2354
+ error?: string;
2355
+ }
2356
+ /**
2357
+ * 호스트-facing 커맨드 채널(비동기) — 씬 컴포넌트 등 클라이언트가 트윈 호스트에 커맨드를 보내는 계약.
2358
+ * 커널 in-process `dispatch`(동기)와 달리 원격 호스트(GraphQL/HTTP/…) 전송을 추상화.
2359
+ * 씬 컨트롤 컴포넌트는 이 인터페이스에만 의존(커널 레벨) — concrete 전송은 각 호스트(things-factory 등)가 주입.
2360
+ * → 컴포넌트가 특정 호스트 구현(things-factory)에 갇히지 않고, 어디서든 개발/재사용 가능.
2361
+ * 컴포넌트는 tenantId 등을 모르는 부분 커맨드를 보내고, 호스트가 보강한다(commandId/type/args 만 제공).
2362
+ */
2363
+ export interface TwinCommandChannel {
2364
+ dispatch(command: Pick<Command, 'commandId' | 'type' | 'args'>): Promise<CommandAck>;
2365
+ }
2366
+ /**
2367
+ * 자극 발생률 — **네 분포 모두 실제로 판정된다**(예전에는 poisson 만 구현되고 나머지는 조용히 상수였다).
2368
+ *
2369
+ * `constant` 간격 일정
2370
+ * `poisson` 무기억 도착(지수 간격) — 실제 도착 과정에 가장 가깝다
2371
+ * `uniform` 0..2×평균 균등 — 평균을 유지하면서 흔들린다
2372
+ * `profile` 시간대별 배율(`profile[시]`)로 도착률을 조절 — 하루 안의 수요 곡선
2373
+ */
2374
+ export interface RateSpec {
2375
+ distribution: 'poisson' | 'uniform' | 'constant' | 'profile';
2376
+ /** 시간당 평균 발생 수. `profile` 이면 여기에 시간대 배율이 곱해진다. */
2377
+ meanPerHour: number;
2378
+ /**
2379
+ * 시간대 배율 — `profile[시]`(0=자정). `distribution: 'profile'` 일 때만 쓰인다.
2380
+ * 배열이 짧으면 **순환**한다(24개=하루, 8개=8시간 주기). 0 이면 그 시간대에는 발생하지 않는다.
2381
+ * 시(hour)는 시뮬 시각 자신의 프레임 — 계약에 표준시가 없으므로 현지 시간대 해석은 하지 않는다.
2382
+ */
2383
+ profile?: number[];
2384
+ }
2385
+ export interface ContentSpec {
2386
+ skuMix: {
2387
+ gtin: string;
2388
+ weight: number;
2389
+ }[];
2390
+ qtyPerLine: {
2391
+ min: number;
2392
+ max: number;
2393
+ };
2394
+ linesPerOrder?: {
2395
+ min: number;
2396
+ max: number;
2397
+ };
2398
+ }
2399
+ export interface GeneratorSpec {
2400
+ /** 도메인 소유 라벨 — 무엇을 생성하는 자극인지 도메인이 명명(OrderState.kind 선례, 무방언). 코어는 강제하지 않음. */
2401
+ kind: string;
2402
+ /**
2403
+ * 코어 라우팅 클래스: 공급(arrival, 물건이 들어옴 → onArrival) vs 수요(order, 요청이 들어옴 → onOrder).
2404
+ * 미지정 시 레거시 kind('outbound-order'→order, 그 외→arrival)로 추론(하위호환, 카탈로그 locationTypes 폴백과 동형).
2405
+ */
2406
+ stimulus?: 'arrival' | 'order';
2407
+ rate: RateSpec;
2408
+ content: ContentSpec;
2409
+ /**
2410
+ * 이 자극이 만드는 오더의 **약속 리드타임(분)** — 생성 시각 + 이 값이 납기(`endTime`)가 된다.
2411
+ *
2412
+ * 표준 `OperationsRequest.EndTime` 을 시나리오가 채우는 경로다. **선언하지 않으면 납기가 없다** —
2413
+ * 그러면 `dueStatusOf` 가 판단하지 않고 지연 신호도 뜨지 않는다(없는 약속을 만들지 않는다).
2414
+ */
2415
+ promisedLeadMinutes?: number;
2416
+ /** 이 자극이 만드는 오더의 우선순위 — 표준 `OperationsRequest.Priority`(작은 값이 급하다). */
2417
+ priority?: number;
2418
+ /**
2419
+ * 운영시간 — 이 구간 밖에서는 자극이 발생하지 않는다(문 닫은 시간에 트럭이 오지 않는다).
2420
+ * `startHour <= endHour` 면 같은 날 구간, 넘어가면 자정을 가로지르는 야간 구간(22→6).
2421
+ * 미지정이면 24시간 가동. 시(hour) 해석은 `RateSpec.profile` 과 같다.
2422
+ */
2423
+ window?: {
2424
+ startHour: number;
2425
+ endHour: number;
2426
+ };
2427
+ }
2428
+ export interface ScenarioDef {
2429
+ seed?: number;
2430
+ speed?: number;
2431
+ horizon?: number;
2432
+ generators: GeneratorSpec[];
2433
+ /**
2434
+ * **가정한 개입** — 시각을 가진 행동들(what-if).
2435
+ *
2436
+ * ── 왜 시나리오에 있나 (2026-08-17) ───────────────────────────────────────
2437
+ * 시나리오는 이미 「우리가 가정한 미래」다. 「30분 뒤 이 설비를 세운다」는 정확히 그 미래의 일부이므로
2438
+ * 여기 있어야 한다. 호스트가 fork 를 굴리며 밖에서 커맨드를 쏘는 방법도 되지만, 그러면 가정이 fork
2439
+ * 밖에 남아 **다른 소비처가 같은 미래를 재생할 수 없다**(예측·백테스트·AI·트윈 모델이 각자 타이밍을 다시
2440
+ * 짜야 한다). fork 가 자기완결이면 같은 선언 하나로 어디서든 같은 미래가 나온다.
2441
+ *
2442
+ * `atMs` 는 **시나리오를 시작한 시점부터의 경과**다(절대 시각이 아니다) — fork 는 언제 갈라져도
2443
+ * 같은 가정을 같은 순서로 겪어야 하고, 절대 시각으로 두면 갈라진 시점에 따라 다른 미래가 된다.
2444
+ *
2445
+ * `kind` 는 커맨드 어휘(`CMD`)를 그대로 쓴다 — 개입은 새 종류의 사건이 아니라 **행동**이고, 그 어휘는
2446
+ * 이미 있다. 커널이 모르는 종류는 거절하고 그 사실을 남긴다(조용히 넘기면 「걸었는데 왜 안 바뀌나」를
2447
+ * 아무도 답할 수 없다).
2448
+ */
2449
+ interventions?: ScenarioIntervention[];
2450
+ /**
2451
+ * **가정한 선언** — 「이렇게 선언돼 있었다면?」(예: 축전지에 피크 억제 임계를 넣으면).
2452
+ *
2453
+ * 개입(`interventions`)이 시각을 가진 **행동**이라면 이쪽은 시각이 없는 **모델의 변주**다. 둘을 한
2454
+ * 자리에 두지 않는 이유: 행동은 저널에 남을 사실이 되고, 변주는 「그런 현장이었다면」이라는 가정이다.
2455
+ *
2456
+ * 시나리오와 함께 다니므로 fork 가 자기완결이다 — 같은 선언 하나로 예측·백테스트·AI 가 같은 미래를
2457
+ * 재생한다. 다만 **실행 중 트윈에 이 시나리오를 걸면 그 트윈의 선언이 실제로 바뀐다**: 그것은
2458
+ * 「우리 현장에 그 자동화가 있다」는 주장이므로, 무엇을 덮어썼는지 소비처가 읽을 수 있어야 한다
2459
+ * (`declarationOverrides()`).
2460
+ */
2461
+ overrides?: ScenarioOverride[];
2462
+ }
2463
+ /** 가정한 선언 하나 — 어느 자원의 어느 속성을 무엇으로. */
2464
+ export interface ScenarioOverride {
2465
+ resourceId: string;
2466
+ propertyId: string;
2467
+ value: string | number;
2468
+ /** 그 자원이 없었으면 false — 조용히 성공한 척하지 않는다. */
2469
+ applied?: boolean;
2470
+ }
2471
+ /** 가정한 개입 하나 — 시각 + 행동. */
2472
+ export interface ScenarioIntervention {
2473
+ /** 시나리오 시작부터의 경과(ms). 0 이면 시작하는 순간. */
2474
+ atMs: number;
2475
+ /** 커맨드 이름(`CMD` 의 값) — 예: `resource.hold`. */
2476
+ kind: string;
2477
+ args?: Record<string, unknown>;
2478
+ }
2479
+ /** 개입이 어떻게 됐나 — 걸린 것과 거절된 것. 조용히 사라지지 않게 소비처가 읽는다. */
2480
+ export interface InterventionOutcome {
2481
+ atMs: number;
2482
+ kind: string;
2483
+ args?: Record<string, unknown>;
2484
+ applied: boolean;
2485
+ /** 거절 이유(커맨드가 낸 코드). 걸렸으면 없다. */
2486
+ refusedCode?: string;
2487
+ }
2488
+ export interface ScenarioControl {
2489
+ load(def: ScenarioDef): void;
2490
+ start(): void;
2491
+ pause(): void;
2492
+ reset(): void;
2493
+ setSpeed(factor: number): void;
2494
+ }
2495
+ export declare const OP_EVENT: {
2496
+ readonly task: "task.status";
2497
+ readonly equipment: "equipment.status";
2498
+ /** 사람 상태 전이 — 설비와 별개 채널(어휘가 다르다: 고장이 아니라 교대·투입). */
2499
+ readonly person: "person.status";
2500
+ /** 물리 자산 상태 전이 — 어디 있나·무엇을 싣고 있나(빈 팔레트인가). */
2501
+ readonly asset: "asset.status";
2502
+ readonly order: "order.status";
2503
+ readonly quality: "quality.output";
2504
+ /**
2505
+ * **시험 결과** — 어느 대상을 어느 기준으로 재고 판정했나.
2506
+ *
2507
+ * `quality.output` 과 **다른 사실이다.** 그것은 생산의 양품·불량 수(가동률 입력)이고, 이것은 선언된
2508
+ * 기준에 대한 검사 판정이다. 한 낱말이 두 일을 하면 어느 쪽 어휘도 옳지 않게 된다.
2509
+ *
2510
+ * **대상을 가리킨다**(`testableObjectId`) — 표준 `TestResult.TestableObjectID` 그대로. 로트·설비·
2511
+ * 사람·자리에 두루 쓰이므로 주체를 이름에 넣지 않았다.
2512
+ *
2513
+ * 이 채널이 없으면 시험 결과는 **상태에만 있는 축**이 된다 — 재기동에서 사라지고, 폴드가 되살릴 수
2514
+ * 없고, 미러가 이어받지 못한다(§상태 ⊆ 이벤트).
2515
+ */
2516
+ readonly test: "test.result";
2517
+ /**
2518
+ * **부적합 처분** — 재고 판정을 받은 것을 어떻게 하기로 정했나(재작업 · 특채 · 폐기 · 반품).
2519
+ *
2520
+ * ── 판정과 다른 사실이다 (2026-08-30) ─────────────────────────────────────
2521
+ * `test.result` 에 필드로 붙이지 않는다. 셋 중 둘을 표현할 수 없게 되기 때문이다.
2522
+ *
2523
+ * 한 판정에 처분이 여럿 일부는 재작업하고 일부는 폐기한다
2524
+ * 처분 없는 판정 기록만 하고 결정은 나중에 한다
2525
+ * 판정 없는 처분 현장 재량으로 뺀다
2526
+ *
2527
+ * ── 왜 커널이 아는가 ──────────────────────────────────────────────────────
2528
+ * 처분은 **자원에 효과를 준다.** 재작업은 자재를 다시 공정에 넣고, 폐기는 재고에서 뺀다. 효과를 주는
2529
+ * 사실은 커널 1급이다.
2530
+ *
2531
+ * EPCIS 의 `disposition` 과 헷갈리지 않는다 — 그것은 **개체의 상태**(`damaged`·`destroyed`)이고
2532
+ * 이것은 **결정**이다. 누가·언제·왜 그렇게 정했는지의 자리는 그쪽에 없다.
2533
+ */
2534
+ readonly disposition: "nonconformance.disposition";
2535
+ /**
2536
+ * **이 목록이 전부다** — 연결된 시스템이 현재 목록을 한 바퀴 다 보낸 뒤 그것을 알린다.
2537
+ *
2538
+ * ── 왜 필요한가 (2026-08-25 실측) ────────────────────────────────────────
2539
+ * 연결된 시스템은 매 주기 현재 재고를 전부 보낸다. 그런데 트윈은 그것을 낱낱의 관측으로 받아
2540
+ * **더하기만 했다.** 「그리고 이것 말고는 없다」를 받는 곳이 없어서, 목록에서 빠진 줄은 아무 말도
2541
+ * 오지 않은 것이 되고 트윈에 영원히 남았다.
2542
+ *
2543
+ * 실측: 실제 재고 1,704건인데 트윈이 10,038건을 갖고 있었다.
2544
+ *
2545
+ * ── 왜 「사라진 것을 알려 주기」가 아니라 이 방식인가 ──────────────────────
2546
+ * 연결 쪽이 앞 주기와 비교해 사라진 줄을 찾아 알리는 방법도 있다. 그런데 그것은 **하나 빠뜨리면
2547
+ * 그 줄이 영원히 남는다.** 이 방식은 주기마다 스스로 바로잡는다 — 이미 잘못 쌓인 것도 다음 주기에
2548
+ * 사라진다.
2549
+ *
2550
+ * ── 보내는 쪽이 지킬 것 ─────────────────────────────────────────────────
2551
+ * **끝까지 읽었을 때만 보낸다.** 읽다가 끊긴 주기에 이것을 보내면 **살아 있는 재고를 지운다.**
2552
+ * 그리고 `since` 는 그 주기를 **시작한** 시각이다(끝낸 시각이 아니다) — 주기 도중에 들어온 관측이
2553
+ * 지워지지 않아야 한다.
2554
+ */
2555
+ readonly complete: "axis.complete";
2556
+ /**
2557
+ * 주목 신호 확인(ack) — **사람이 한 행위**라 파생될 수 없다.
2558
+ *
2559
+ * 다른 파생 상태는 상태에서 다시 계산된다(주목 신호 자체가 그렇다). 그런데 "누가 이것을 봤다" 는
2560
+ * 계산으로 되살릴 수 없다. 저널에 남기지 않으면 재기동하면 확인해 둔 신호가 다시 빨개지고,
2561
+ * 과거를 다시 계산해도 그때 무엇을 확인했는지 알 수 없다 — 저널이 현실을 불완전하게 담는 자리였다.
2562
+ */
2563
+ readonly attentionAck: "attention.acked";
2564
+ /**
2565
+ * **자리에서 관측된 물리량** — 냉장실 온도·습도, 세척수 유량 같은 것(§`LocationObservation`).
2566
+ *
2567
+ * ── 왜 채널이 필요한가 ────────────────────────────────────────────────────
2568
+ * `LocationState.observations` 를 상태에 두었는데 그것을 낳는 사건이 없었다. 그러면 **상태 ⊆ 이벤트**
2569
+ * 가 깨진다: 채워도 재기동에서 사라지고, 폴드가 되살릴 수 없고, 미러가 이어받을 수도 없다. 상태에만
2570
+ * 있는 축은 조용히 사라지는 축이다.
2571
+ *
2572
+ * 이름을 `energy.measured` 와 같은 결로 둔다 — 그쪽이 에너지 계량의 도착이고 이쪽이 그 일반형이다.
2573
+ * 두 채널을 합치지 않는 이유는 에너지 쪽이 **구간에 누적되는 표본**이라 처리가 다르기 때문이다.
2574
+ */
2575
+ readonly observation: "location.measured";
2576
+ };
2577
+ /**
2578
+ * 에너지 트윈의 사건 — **물(物)의 계보가 아니라 스칼라의 시계열.**
2579
+ *
2580
+ * ── 왜 EPCIS 어휘를 쓰지 않나 ──────────────────────────────────────────────
2581
+ * EPCIS 는 "무엇이 어디서 무엇에 일어났나" 를 물건 단위로 말한다. 에너지에는 옮겨 다니는 물건이
2582
+ * 없다 — 계량 지점에서 수치가 변할 뿐이다. 억지로 ObjectEvent 로 감싸면 화면이 "전력 한 개가
2583
+ * 이동했다" 로 읽는다. 그래서 자기 `eventType` 을 갖되 **봉투는 같은 것**을 쓴다
2584
+ * (`CanonicalEnvelope { eventType, data }`) — 저널·리플레이·시간여행을 그대로 얻는다.
2585
+ *
2586
+ * ── 왜 지금 사건만 선언하나 ────────────────────────────────────────────────
2587
+ * 상태 필드(계량 지점의 kW·구간의 계약전력)와 능력(`metered`·`curtailable`)은 **채우는 쪽이
2588
+ * 생길 때** 함께 선언한다. 선언에만 자리를 두고 아무도 옮기지 않으면 그것이 이 프로젝트가 하네스로
2589
+ * 막아 온 바로 그 결함이다(`declaration-in-state`). 사건은 그 검사의 대상이 아니고, 커넥터·커널이
2590
+ * 무엇을 주고받을지 먼저 합의해야 하는 값이라 여기가 그 자리다.
2591
+ *
2592
+ * 설계 근거: `design/profiles/ems.md` §4.
2593
+ */
2594
+ export declare const ENERGY_EVENT: {
2595
+ /** 계량 도착 — 그 시점의 유효전력·누적량. 15분 수요 구간에 누적된다. */
2596
+ readonly measured: "energy.measured";
2597
+ /**
2598
+ * 설비의 에너지 상태 — 발전·저장·감축 여지·개폐 위치.
2599
+ *
2600
+ * 계량과 다른 사건으로 둔다: 계량은 구간에 **누적**되고 이것은 그 설비의 **지금**을 바꾼다.
2601
+ */
2602
+ readonly equipment: "energy.equipment";
2603
+ /** 수요 구간 마감 — 그 구간의 최대 수요가 확정된다(요금의 단위). */
2604
+ readonly demandWindow: "energy.demand.window";
2605
+ /**
2606
+ * **마감된 사용 구간이 도착했다** — 우리가 마감한 것이 아니라 밖에서 확정되어 온 것이다.
2607
+ *
2608
+ * 계량 표본이 오지 않는 현장이 있다. 공급자의 조회나 계량 데이터 시스템이 「이 구간에 X 썼다」를
2609
+ * 이미 마감해 준다. 그 값을 적산(`energy.measured` 의 `kWh`)으로 보내면 커널이 그것을 누적으로
2610
+ * 읽고 차분을 또 구한다 — 차분의 차분이 되어 틀린다. 그래서 다른 사건이다.
2611
+ *
2612
+ * `demandWindow` 와 **합치지 않는다.** 하나는 우리가 표본에서 마감한 것이고 이것은 밖에서 온
2613
+ * 것이다. 두 회계를 더하면 같은 전기를 두 번 센다. 둘 다 있으면 읽는 쪽이 하나를 고르고 어느
2614
+ * 쪽인지 말한다.
2615
+ */
2616
+ readonly usagePeriod: "energy.usage.period";
2617
+ /**
2618
+ * **발전에 매겨지는 단가가 정해졌다** — 이 기간에 낸 전기 1kWh 의 값.
2619
+ *
2620
+ * 소비에 요금이 매겨지듯 발전에도 값이 매겨진다. 파는 발전소는 판 값이고, 자가소비형은 아꼈다고
2621
+ * 볼 수 있는 값이다 — 어느 쪽인지는 현장이 정한다.
2622
+ *
2623
+ * **단가는 날마다 바뀐다.** 그래서 선언이 아니라 기간을 가진 사실로 받는다. 선언에 적어 두면
2624
+ * 사람이 매일 갱신해야 하고, 안 하면 낡은 값으로 수익이 조용히 틀린다.
2625
+ *
2626
+ * 사건 시각은 기간의 시작이다 — 「이 시각부터 이 단가가 적용된다」이므로 기간이 끝나기 전에 성립한다.
2627
+ *
2628
+ * ── 담지 않은 것 ────────────────────────────────────────────────────────────
2629
+ * 제도마다 있는 가산·인증서·가중치는 담지 않는다. 그 셈은 제도가 정하고 나라마다 다르므로,
2630
+ * 커넥터가 계산해 **1kWh 당 얼마**로 옮겨 보낸다. 커널이 한 제도의 모양을 안으면 다른 제도에서
2631
+ * 그 자리가 거짓이 된다.
2632
+ */
2633
+ readonly generationPrice: "energy.generation.price";
2634
+ /**
2635
+ * **이 주기에 적용되는 요금 기준이 정해졌다** — 요금적용전력과 기본요금 단가.
2636
+ *
2637
+ * 청구서와 다른 사건이다. 청구서는 끝난 기간의 정산이고 이것은 **지금 적용되는 기준**이다.
2638
+ * 그래서 사건 시각도 다르다 — 청구서는 기간의 끝, 이것은 기간의 시작이다(그때부터 유효하다).
2639
+ *
2640
+ * 이 값은 매 주기 다시 정해진다. 모델의 선언에 적어 두면 사람이 매달 갱신해야 하고, 안 하면 낡은
2641
+ * 값으로 기본요금이 조용히 틀린다. 공급자가 알려 주는 값을 그대로 받는 것이 맞다.
2642
+ *
2643
+ * **금액을 싣지 않는다.** 요금적용전력 × 단가는 커널이 계산한다. 곱을 받으면 그것이 정산인 척한다.
2644
+ */
2645
+ readonly tariffBasis: "energy.tariff.basis";
2646
+ /**
2647
+ * **청구서가 도착했다** — 공급자가 확정한 금액.
2648
+ *
2649
+ * 우리가 계산한 금액(§`electricityCost`)을 덮지 않는다. 두 값은 다른 것이다: 청구는 사실이고 우리
2650
+ * 계산은 파생이다(구간 요금·예측·가정 비교에 쓴다). 계기 적산과 같은 규율이다 — 다를 때 맞는 쪽은
2651
+ * 그 회계를 가진 쪽이다.
2652
+ *
2653
+ * 어긋나면 **그 사실을 낸다.** 우리 계산이 틀렸다는 뜻이고, 그것을 사람이 찾아내지 않아도 드러나야
2654
+ * 한다(실제로 사람이 찾아낸 적이 있다 — 기본요금을 이 주기의 최대로 매기고 있었다).
2655
+ */
2656
+ readonly bill: "energy.bill";
2657
+ /**
2658
+ * 피크 경신 — 월 최대 수요가 갱신됐다.
2659
+ *
2660
+ * 구간 마감에서 파생되지만 **사실로 남긴다**: 요금의 근거이고, 나중에 "언제 무엇 때문에 올랐나" 를
2661
+ * 물을 때 파생으로는 답할 수 없다(그 순간의 부하 구성이 사라진다).
2662
+ */
2663
+ readonly peak: "energy.peak";
2664
+ /** 요금 구간 전환 — 경부하·중간부하·최대부하. 달력이 아니라 사건이다(계절제·특례로 바뀐다). */
2665
+ readonly tariffShift: "energy.tariff.shift";
2666
+ /** 발전(PV 등) — 역송을 포함한다(음의 소비가 아니라 별개 사실이다). */
2667
+ readonly generated: "energy.generated";
2668
+ /**
2669
+ * **기간 발전량 확정** — 한 기간이 끝났고 그 기간에 얼마 냈는지가 정해졌다.
2670
+ *
2671
+ * ISO 50001 의 성과지표와 기준선, 태양광 성능비가 모두 「기간당 에너지」로 정의된다. 적산값만 상태에
2672
+ * 두면 그 어느 것도 계산할 수 없고, 지난 기간들을 합할 수도 없다(상태는 지금 하나만 든다).
2673
+ *
2674
+ * 계량이 수요 구간을 마감해 사실로 내는 것과 같은 짝이다(§`demandWindow`). 두 값을 더하지 말 것 —
2675
+ * 발전량은 설비가 만든 양이고 구간 전력량은 계량 지점을 지난 양이다.
2676
+ */
2677
+ readonly generationPeriod: "energy.generation.period";
2678
+ /** 저장(ESS 충전). */
2679
+ readonly stored: "energy.stored";
2680
+ /** 방전(ESS). 충전과 나눈다 — 손실·수명 판단이 둘을 구별해야 한다. */
2681
+ readonly discharged: "energy.discharged";
2682
+ /**
2683
+ * 수요 제어 제안 — **제안이지 명령이 아니다.**
2684
+ *
2685
+ * 이 트윈은 차단·투입을 하지 않는다(안전 계통은 범위 밖: `ems.md` §1). 이대로면 계약을 넘는다는
2686
+ * 판단과 무엇을 줄이면 되는지를 낼 뿐이고, 집행은 사람과 그 시스템의 몫이다. 이름을 `suggested`
2687
+ * 로 둔 이유가 그것이다 — 저널만 보고도 "우리가 끈 것이 아니다" 를 알 수 있어야 한다.
2688
+ */
2689
+ readonly drSuggested: "energy.dr.suggested";
2690
+ };
2691
+ export type EnergyEventType = (typeof ENERGY_EVENT)[keyof typeof ENERGY_EVENT];
2692
+ /**
2693
+ * 설비가 낸 **자기 에너지 상태** — 계량이 아닌 능력들의 값.
2694
+ *
2695
+ * 계량(`energy.measured`)과 갈라 두는 이유: 계량은 **구간에 누적되는 표본**이고, 이것들은 **그 설비의
2696
+ * 지금 상태**다. 같은 문으로 넣으면 발전량이 부하 구간에 더해지거나 충전율이 요금 판정에 섞인다.
2697
+ *
2698
+ * 한 봉투에 여러 능력의 값이 함께 올 수 있다(원 시스템이 한 번에 준다) — 각 값은 그것을 선언한 능력의
2699
+ * 필드로만 들어간다. 없는 값은 **보내지 않는다**(0 을 보내면 「그렇게 측정됐다」가 된다).
2700
+ */
2701
+ export interface EnergyEquipmentData {
2702
+ equipmentId: string;
2703
+ at: ISOTime;
2704
+ generatedKW?: number;
2705
+ exportKW?: number;
2706
+ soc?: number;
2707
+ chargeKW?: number;
2708
+ dischargeKW?: number;
2709
+ curtailable?: boolean;
2710
+ minKW?: number;
2711
+ position?: 'open' | 'closed' | 'intermediate' | 'bad';
2712
+ /**
2713
+ * **전기 계측** — 전압(V)·전류(A). 전기를 만들거나 쓰는 어떤 설비에서나 같은 모양이다
2714
+ * (IEC 61850 의 계측 항목).
2715
+ *
2716
+ * ── 왜 kW 만으로 부족한가 (2026-08-29) ─────────────────────────────────────
2717
+ * 출력이 0 일 때 **왜 0 인지**를 kW 는 말하지 못한다. 전압과 전류가 있으면 값이 답한다.
2718
+ *
2719
+ * 전압 있음 · 전류 0 회로가 끊겼다 (실측: 직류 621.9V · 0A)
2720
+ * 전압 0 · 전류 0 원천이 없다 (밤·차단)
2721
+ * 전압 낮음 · 전류 높음 부하가 무겁거나 계통이 약하다
2722
+ *
2723
+ * ── 직류와 교류를 가른다 ────────────────────────────────────────────────────
2724
+ * 변환기(인버터·정류기)를 낀 설비는 양쪽이 다른 값이고, 어느 쪽이 죽었는지가 곧 어디가 고장인지다.
2725
+ * 한 이름에 담으면 그 구별이 사라진다.
2726
+ *
2727
+ * ── 상별로 받는다 ──────────────────────────────────────────────────────────
2728
+ * 상 불평형은 그 자체로 사실이고, 합치면 되돌릴 수 없다. 단상이면 원소 하나짜리 배열이다.
2729
+ *
2730
+ * **역률은 이 자리에 없다.** 뜻을 확인하지 못한 값을 받으면 화면이 그것을 역률이라고 말한다.
2731
+ */
2732
+ dcVoltage?: number;
2733
+ dcCurrent?: number;
2734
+ acVoltage?: number[];
2735
+ acCurrent?: number[];
2736
+ }
2737
+ /**
2738
+ * 발전 적산으로 담기는 값 — **발전 설비 하나가 지금까지 만든 양.**
2739
+ *
2740
+ * ── 왜 계량과 따로 있나 (2026-08-26) ───────────────────────────────────────
2741
+ * 이 사건 이름(`energy.generated`)은 오래 **선언만 되어 있었다.** 설계 문서(`profiles/ems.md` §4)와
2742
+ * 이 표에 이름이 있고, 내는 곳이 한 곳도 없었다. 그래서 태양광 발전소를 붙인 현장에서 「지금 발전
2743
+ * 중」은 보이고 「얼마나 발전했나」는 보이지 않았다(호현에너지 실측: 하루에 174kWh 가 자랐는데 트윈에는
2744
+ * 없었다).
2745
+ *
2746
+ * 계량(`EnergyMeasuredData`)으로 보낼 수는 없다. 계량 지점의 값은 부하 합에 들어가고(§`MeterPointState`),
2747
+ * 발전을 그 합에 넣으면 계약 대비 판단이 반대로 틀린다. 발전은 **음의 소비가 아니라 별개 사실**이다 —
2748
+ * 이 표의 `generated` 주석이 처음부터 그렇게 적어 두었다.
2749
+ *
2750
+ * ── 왜 순시 출력(kW)은 여기 없나 ───────────────────────────────────────────
2751
+ * 그것은 이미 갈 곳이 있다(`EnergyEquipmentData.generatedKW`). 같은 값을 두 문으로 받으면 어느 쪽이
2752
+ * 맞는지 정하는 규칙이 또 필요해진다. 이 문은 **적산만** 받는다 — 그것이 없던 것이다.
2753
+ */
2754
+ export interface EnergyGeneratedData {
2755
+ /** 발전 설비(id) — 계량 지점이 아니다. */
2756
+ equipmentId: string;
2757
+ /**
2758
+ * 계기 적산값(kWh) — **연결된 시스템이 준 값 그대로.** 차분은 소비처가 한다.
2759
+ *
2760
+ * 우리가 kW 를 적분해 만들지 않는다. 적산은 그 계기 자신의 회계이고, 우리가 적분한 값과 미세하게
2761
+ * 다르며 다를 때 맞는 쪽은 계기다(계량 쪽과 같은 규율).
2762
+ */
2763
+ kWh: number;
2764
+ /**
2765
+ * **이 적산이 쌓이기 시작한 시각** — 원본이 말했을 때만 있다 (2026-08-28).
2766
+ *
2767
+ * ── 왜 필요한가 ─────────────────────────────────────────────────────────────
2768
+ * 계기가 주는 적산이 어느 구간의 것인지는 **원본마다 다르다** — 설치 이후·하루·한 달·요금기간.
2769
+ * 표준도 그것을 갈라 둔다(계측값에 구간을 붙인다). 그런데 이 문은 한 뜻만 가정하고 있었다:
2770
+ * 단조 증가하는 총적산. 그래서 값이 줄면 **계기 교체로 추론했다.**
2771
+ *
2772
+ * 하루 누적을 주는 원본에서는 그 추론이 매일 자정마다 참이 된다 — 실측(PPMS 인버터):
2773
+ *
2774
+ * 2026-08-25 23:37 KST acc 574.8
2775
+ * 2026-08-28 18:25 KST acc 165.2 ← 줄었다. 자정에 되돌아간다
2776
+ *
2777
+ * 그대로 받으면 「계기 교체가 하루에 한 번 일어난다」가 기록된다.
2778
+ *
2779
+ * ── 왜 시각인가(구간의 이름이 아니라) ───────────────────────────────────────
2780
+ * 한 축으로 하루·한 달·요금기간·설치 이후를 다 담고, 커널이 구간을 이어 붙일 근거도 그 시각이다.
2781
+ * 「요금기간」 같은 이름은 경계를 계약이 정하므로 이름만으로는 커널이 알 수 없다.
2782
+ *
2783
+ * **없으면 「모른다」다** — 설치 이후로 가정하지 않는다. 그때 커널은 지금처럼 추론하고, 그 추론이
2784
+ * 짐작이라는 사실을 상태에 남긴다(§`generatedKWhBasis`).
2785
+ */
2786
+ since?: ISOTime;
2787
+ /**
2788
+ * **어떤 누적인가** — 표준이 값의 형에 두는 축이다 (2026-08-28).
2789
+ *
2790
+ * ── 왜 구간의 시작만으로 부족한가 ───────────────────────────────────────────
2791
+ * 처음에는 `since`(구간의 시작) 하나만 두었다. 그러면 **원본이 그것을 모를 때 아무 표도 남지 않는다** —
2792
+ * 소비처는 `574.8` 만 보고 그것이 하루치인지 평생치인지 알 방법이 없다. 되돌아가야 비로소 「모른다」가
2793
+ * 남는데(§`generatedKWhBasis`), 그전까지는 그 값을 다른 값과 비교해도 아무도 막지 않는다.
2794
+ *
2795
+ * 이 트윈의 규율은 **모르는 것을 모른다고 말하는 것**이다. 그래서 모름도 값으로 둔다.
2796
+ *
2797
+ * ── 이 축의 근거 ────────────────────────────────────────────────────────────
2798
+ * **표준이 강제해서 두는 축이 아니다.** 근거는 위 문단 하나로 끝난다 — 기준점을 모르는 값을 받으면
2799
+ * 커널이 추론하고, 추론한 것이 사실로 적힌다. 종류와 구간은 겹치지 않는 물음에 답한다: 종류는
2800
+ * 「견줄 수 있나」, 시각은 「어느 구간인가」.
2801
+ *
2802
+ * 곁근거로 IEC 61968-9 의 `ReadingType` 이 계측값의 성질을 여러 축으로 나눈다고 알고 있다(누적 방식·
2803
+ * 거시 구간·방향). **다만 그 문서를 읽지 않았다** — 유료 표준이고 이 저장소에 사본이 없다. 축 이름은
2804
+ * 기억이므로 이 주석을 근거로 인용하지 말 것. 커널이 **채택 선언한** 에너지 표준은 IEC 61850(설비·
2805
+ * 계측 모델)과 ISO 50001(경영)이다(§`StandardClass`).
2806
+ *
2807
+ * 어느 표준도 「적산은 평생치다」라고 말하지 않는다. 그 전제는 커널의 판정에 있었다 —
2808
+ * 값이 줄면 계기의 이상으로 읽는 것(§`generatedKWhResetAt`). 그것이 매일 자정을 계기 교체로
2809
+ * 적게 만든 자리다.
2810
+ *
2811
+ * 'lifetime' 설치 이후 — 단조 증가한다. 줄면 계기의 이상이다
2812
+ * 'daily' 그날부터
2813
+ * 'monthly' 그달부터
2814
+ * 'billing' 요금기간부터 — 경계는 계약이 정하므로 시각을 함께 받아야 구간이 정해진다
2815
+ * 'unknown' **원본이 말하지 않는다.** 이 값을 다른 값과 비교하지 말라는 뜻이다
2816
+ *
2817
+ * 없으면(칸이 아예 없으면) 커널은 `'unknown'` 으로 둔다 — 예전 판이 보내던 레코드가 조용히 평생치로
2818
+ * 읽히지 않게.
2819
+ */
2820
+ accumulation?: 'lifetime' | 'daily' | 'monthly' | 'billing' | 'unknown';
2821
+ at: ISOTime;
2822
+ }
2823
+ /** 계량 도착의 실린 값 — 계량 지점 하나의 한 시점. */
2824
+ export interface EnergyMeasuredData {
2825
+ /** 계량 지점(설비 id). */
2826
+ meterId: string;
2827
+ /** 유효전력(kW). 순시가 아니라 그 계량 주기의 평균이다. */
2828
+ kW: number;
2829
+ /** 누적 전력량(kWh) — 계기 누적값. 차분은 소비처가 한다(계기 교체·리셋을 우리가 지어내지 않는다). */
2830
+ kWh?: number;
2831
+ /**
2832
+ * 이 적산이 쌓이기 시작한 시각 — 원본이 말했을 때만 있다(§`EnergyGeneratedData.since`).
2833
+ *
2834
+ * 계량 쪽의 셈은 이 값 없이도 정직하다 — 적산이 줄면 그 구간의 전력량을 **내지 않는다**(음수를
2835
+ * 만들지 않는다). 이 축은 셈을 바꾸지 않고 **왜 비는지**를 말할 수 있게 한다: 「계기가 교체돼서」와
2836
+ * 「구간이 바뀌어서」는 다른 사실이고, 화면이 그 둘을 같은 빈칸으로 보이면 사람이 계기를 의심한다.
2837
+ */
2838
+ kWhSince?: ISOTime;
2839
+ /** 역률 — 없으면 모르는 것이다(1 로 채우지 않는다). */
2840
+ powerFactor?: number;
2841
+ at: ISOTime;
2842
+ }
2843
+ /** 수요 구간 마감 — 계약 대비 판정의 단위. */
2844
+ /**
2845
+ * 기간 발전량 확정 — `ENERGY_EVENT.generationPeriod` 의 데이터.
2846
+ *
2847
+ * 관측 구간을 함께 싣는 이유: 기간의 끝이 자정인데 마지막 표본이 저녁 여덟 시였다면, 그 사이는 재지
2848
+ * 않은 것이다. 태양광이면 대개 0 이지만 그것은 현장이 판단할 일이고, 재지 않은 것을 0 으로 적으면
2849
+ * 우리가 그 판단을 대신한 것이 된다.
2850
+ */
2851
+ /**
2852
+ * 사실의 **주체를 무엇으로 불렀나** — 그 이름이 어디까지 통하는지가 여기서 갈린다.
2853
+ *
2854
+ * ── 왜 이 축이 필요한가 (2026-08-30) ──────────────────────────────────────
2855
+ * 마감된 구간 사실은 정체성이 내용에 있다(종류·주체·시작·끝). 그런데 **주체를 설비 번호로 불렀다.**
2856
+ * 설비 번호는 한 트윈 안에서만 통하는 이름표라, 한 도메인에 현장이 둘이면 서로 다른 설비의 같은 날이
2857
+ * **한 사실**이 된다. 실측으로 그 일이 났다 — 두 발전소의 `002` 가 겹쳐, 여러 현장을 함께 계산하는
2858
+ * 성과 화면에서 한쪽 발전량이 사라졌다.
2859
+ *
2860
+ * 고치는 방법은 두 가지였고 하나는 틀렸다. **트윈 id 를 이름에 붙이는 것은 그릇으로 정체성을 정하는
2861
+ * 것**이라 원칙을 뒤집는다(같은 설비를 현장 트윈과 상위 집계 트윈에 두면 같은 날이 두 사실이 된다).
2862
+ *
2863
+ * 그래서 이름의 출처를 밝힌다.
2864
+ *
2865
+ * declared 모델이 선언한 설비 정체성 — 그릇과 무관하게 고유하다
2866
+ * twin-local 선언이 없어 이름표를 그대로 썼다 — **이 트윈 안에서만 통한다**
2867
+ *
2868
+ * 커널은 없는 이름을 지어내지 않고, 겹치는 이름을 고유한 척하지도 않는다. 읽는 쪽은 `twin-local` 을
2869
+ * 볼 때 트윈 밖에서 같다고 판정하면 안 된다.
2870
+ */
2871
+ export type SubjectBasis = 'declared' | 'twin-local';
2872
+ export interface EnergyGenerationPeriodData {
2873
+ equipmentId: string;
2874
+ /** 이 기간에 만든 양(kWh). */
2875
+ kWh: number;
2876
+ periodStart: ISOTime;
2877
+ periodEnd: ISOTime;
2878
+ /** 이 기간에서 처음·마지막으로 관측한 시각. 기간 경계와 다르면 그만큼 재지 않았다. */
2879
+ observedFrom: ISOTime;
2880
+ observedTo: ISOTime;
2881
+ /** 원본이 준 적산의 종류 — 총량을 어떻게 얻었는지가 이것으로 갈린다. */
2882
+ accumulation: 'lifetime' | 'daily' | 'monthly' | 'billing';
2883
+ /** 기간의 경계가 어디서 왔나 — 원본의 기준점(`declared`)이거나 현장의 시각 기준(`offset`). */
2884
+ boundary: 'declared' | 'offset';
2885
+ /** 이 사실이 무엇에 대한 것인가 — 정체성으로 부른 이름(§`SubjectBasis`). */
2886
+ subject?: string;
2887
+ subjectBasis?: SubjectBasis;
2888
+ }
2889
+ /**
2890
+ * 값이 어디서 왔나 — **관측의 근거**.
2891
+ *
2892
+ * 같은 kWh 라도 계량기가 잰 것과 공급자 화면에서 긁어온 것은 무게가 다르다. 실제로 긁어온 값이
2893
+ * 망가져 있는 것을 본 적이 있다(다른 현장의 값이 섞이고, 세 자리 수가 한 자리로 왔다). 근거를 값과
2894
+ * 함께 두면 화면이 「이 수는 계량이 아니다」를 말할 수 있다.
2895
+ *
2896
+ * 커널은 이 값으로 판정하지 않는다 — 무엇을 믿을지는 현장이 정한다.
2897
+ */
2898
+ export declare const OBSERVATION_BASIS: readonly ["metered", "provider", "billed", "estimated"];
2899
+ export type ObservationBasis = (typeof OBSERVATION_BASIS)[number];
2900
+ /**
2901
+ * 마감된 사용 구간 — `ENERGY_EVENT.usagePeriod` 의 데이터.
2902
+ *
2903
+ * 적산이 아니라 **그 구간에 쓴 양**이다. 구간의 단가를 함께 실을 수 있다: 시간대·계절마다 단가가
2904
+ * 다른 현장에서 하나로 접으면 「피크를 깎아 얼마를 아끼나」가 사라진다.
2905
+ */
2906
+ export interface EnergyUsagePeriodData {
2907
+ meterId: string;
2908
+ from: ISOTime;
2909
+ to: ISOTime;
2910
+ /** 이 구간에 쓴 양(kWh). */
2911
+ kWh: number;
2912
+ /** 이 구간의 최대 수요(kW) — 원본이 함께 줄 때만. */
2913
+ maxKW?: number;
2914
+ /** 이 구간에 적용된 단가(1kWh 당). 통화는 `currency`. */
2915
+ unitPrice?: number;
2916
+ currency?: string;
2917
+ basis?: ObservationBasis;
2918
+ /** 이 사실이 무엇에 대한 것인가 — 계량 지점도 설비와 같은 문제를 갖는다(§`SubjectBasis`). */
2919
+ subject?: string;
2920
+ subjectBasis?: SubjectBasis;
2921
+ }
2922
+ /**
2923
+ * 청구서 — `ENERGY_EVENT.bill` 의 데이터.
2924
+ *
2925
+ * **커널의 요금 어휘로 받는다**(사용량 요금·기본요금·합계). 공급자마다 청구서의 칸 이름이 다르므로
2926
+ * 그 이름을 그대로 들이면 커널이 한 공급자의 모양에 묶인다. 옮기는 것은 커넥터의 일이다.
2927
+ */
2928
+ /**
2929
+ * 이 주기에 적용되는 요금 기준 — `ENERGY_EVENT.tariffBasis` 의 데이터.
2930
+ *
2931
+ * 사건 시각은 `from` 이다. 「이 시각부터 이 기준이 적용된다」이므로 주기가 끝나기 전에 성립하고,
2932
+ * `to` 로 시각을 잡으면 아직 오지 않은 시각이 저널에 적힌다.
2933
+ */
2934
+ /**
2935
+ * 발전 단가 — `ENERGY_EVENT.generationPrice` 의 데이터.
2936
+ *
2937
+ * 사건 시각은 `from` 이다(그때부터 적용된다). 가산·인증서 같은 제도의 셈은 커넥터가 하고, 여기에는
2938
+ * **1kWh 당 얼마**만 온다.
2939
+ */
2940
+ export interface EnergyGenerationPriceData {
2941
+ from: ISOTime;
2942
+ to: ISOTime;
2943
+ /** 1kWh 당 값. 통화는 `currency`. */
2944
+ unitPrice: number;
2945
+ currency: string;
2946
+ }
2947
+ export interface EnergyTariffBasisData {
2948
+ from: ISOTime;
2949
+ to: ISOTime;
2950
+ /** 기본요금이 걸리는 기준(kW) — 공급자가 정한다(§`TariffDeclaration.billingDemandKW`). */
2951
+ billingDemandKW?: number;
2952
+ /** 그 기준 1kW 당 기본요금. */
2953
+ demandChargePerKW?: number;
2954
+ currency?: string;
2955
+ }
2956
+ /**
2957
+ * 청구서 — 공급자가 **정산한** 금액.
2958
+ *
2959
+ * ── 지금 이 문으로 오는 것이 없다 (2026-08-29 확인) ────────────────────────
2960
+ * 붙어 있는 원본에 정산 금액을 주는 조회가 없다. 그 원본이 주는 것은 이 주기의 **요금 기준**이고
2961
+ * 그것은 다른 문으로 받는다(§`EnergyTariffBasisData`).
2962
+ *
2963
+ * 그 둘을 한 문으로 받으면 **우리가 곱한 값을 우리가 다시 빼는** 비교가 된다 — 실제로 한 번 그렇게
2964
+ * 했고, 차이가 0 인 것을 계산이 맞았다는 확인으로 읽었다. 같은 두 값을 두 번 곱한 것이었다.
2965
+ */
2966
+ export interface EnergyBillData {
2967
+ from: ISOTime;
2968
+ to: ISOTime;
2969
+ /** 사용량 요금 · 기본요금 · 합계 — 청구서가 말한 값. 없는 것은 비운다(0 으로 채우지 않는다). */
2970
+ energyCharge?: number;
2971
+ demandCharge?: number;
2972
+ total?: number;
2973
+ currency?: string;
2974
+ /** 그 청구가 기본요금을 매긴 기준(kW) — 우리 선언과 다르면 선언이 낡은 것이다. */
2975
+ billingDemandKW?: number;
2976
+ }
2977
+ export interface EnergyDemandWindowData {
2978
+ /** 배전 구간(로케이션 id) 또는 현장 전체. */
2979
+ feederId?: string;
2980
+ windowStart: ISOTime;
2981
+ windowEnd: ISOTime;
2982
+ /** 그 구간의 최대 수요(kW). */
2983
+ peakKW: number;
2984
+ /** 계약전력(kW) — 선언에서 온다. 없으면 판정하지 않는다(넘었는지 말할 수 없다). */
2985
+ contractKW?: number;
2986
+ }
2987
+ /**
2988
+ * 주목 신호 확인 델타 — 확인한 신호의 id 와 시각.
2989
+ *
2990
+ * 신호의 내용은 싣지 않는다(상태에서 다시 계산된다). 여기 남기는 것은 **사람이 확인했다는 사실** 하나다.
2991
+ */
2992
+ export interface AttentionAckDelta {
2993
+ id: string;
2994
+ /** 확인한 시각 — 없으면 이벤트 시각을 쓴다. */
2995
+ at?: ISOTime;
2996
+ }
2997
+ /** 사람 상태 델타 — 배정·해제·교대 전이 시 방출. 미러가 인원 가용을 비추는 근거. */
2998
+ export interface PersonStatusDelta extends EffectivePeriod {
2999
+ personId: string;
3000
+ personnelClassIds?: string[];
3001
+ status: string;
3002
+ taskId?: string;
3003
+ offShift?: boolean;
3004
+ /** 지금 어디에 있나 — 나가지 않으면 미러가 사람의 위치를 영영 모른다(상태⊆이벤트). */
3005
+ location?: string;
3006
+ }
3007
+ /** 물리 자산 상태 델타 — 이동·투입·적재/하역 시 방출. 미러가 자산 가용을 비추는 근거. */
3008
+ export interface AssetStatusDelta extends EffectivePeriod {
3009
+ assetId: string;
3010
+ assetClassIds?: string[];
3011
+ status: string;
3012
+ location?: string;
3013
+ taskId?: string;
3014
+ carrying?: string;
3015
+ }
3016
+ /** 품질 산출 델타 — recordOutput(양품/불량) 시 방출. goodCount/scrapCount 는 설비 누적값. */
3017
+ export interface QualityDelta {
3018
+ moverId: string;
3019
+ good: boolean;
3020
+ goodCount: number;
3021
+ scrapCount: number;
3022
+ }
3023
+ export interface TaskStatusDelta {
3024
+ taskId: string;
3025
+ /** 착수 시각(절대 sim-clock) — 보간 앵커. TaskState 와 같은 뜻. */
3026
+ startedAtSimMs?: number;
3027
+ /** 투입된 사람들 — 미러가 인원 배정을 그대로 비추려면 델타에 실려야 한다. */
3028
+ personnel?: string[];
3029
+ /** 투입된 물리 자산(팔레트 등). */
3030
+ assets?: string[];
3031
+ /**
3032
+ * 투입된 설비 전부 — `resourceRef` 는 그중 **대표**(첫 번째)다.
3033
+ * 대표만 두면 "크레인 + 스프레더" 처럼 함께 잡히는 설비가 상태에서 사라진다.
3034
+ */
3035
+ resources?: string[];
3036
+ orderId?: string;
3037
+ kind: string;
3038
+ status: TaskStatus;
3039
+ fromNode?: string;
3040
+ toNode?: string;
3041
+ itemRefs?: string[];
3042
+ resourceRef?: string;
3043
+ /**
3044
+ * 작업 의도 — transport(운반) · process(가공) · dwell(체류).
3045
+ *
3046
+ * 없으면 소비처가 "자원이 없는 것" 과 "자원을 쓰지 않는 공정" 을 **구별할 수 없다**(체류는 무자원이
3047
+ * 정상인데 데이터 유실로 읽힌다 — 2026-07-31 에 화면이 그렇게 말할 뻔했다).
3048
+ */
3049
+ intent?: 'transport' | 'process' | 'dwell';
3050
+ /**
3051
+ * 진척(0~1)과 남은 시간(ms) — **진행 중인 작업을 이어서 실행하려면 필요하다.**
3052
+ *
3053
+ * 없으면 미러 상태를 씨앗으로 한 예측이 "진행 중인 일이 하나도 없는 현장" 에서 출발한다.
3054
+ * 완료·생성 시점에는 의미가 없어 in-progress 에만 실린다.
3055
+ */
3056
+ progress?: number;
3057
+ remainingMs?: number;
3058
+ /** 이 작업의 총 소요 예상(ms) — 진척의 분모. */
3059
+ durationMs?: number;
3060
+ /**
3061
+ * 품질 판정 결과 — **판정이 있었던 작업만** (2026-08-19).
3062
+ *
3063
+ * 왜 계약에 있나: 양품/불량은 설비 누적 카운터(종류를 모른다)와 EPCIS disposition(종류를 모른다)
3064
+ * 에만 남아서, **「이 공정의 양품률이 얼마였나」를 저널에서 되짚을 수 없었다** — 재기동하면 종류별
3065
+ * 품질 이력이 통째로 사라졌다. 판정이 일어나는 자리(작업 완료)에서 사실을 적는다.
3066
+ *
3067
+ * **없음은 「양품」이 아니라 「판정하지 않았다」**다(이동·체류에는 품질 판정이 없다). 그래서 소비처는
3068
+ * 없는 것을 세지 않는다 — 모름을 양품으로 세면 양품률이 조용히 100% 로 올라간다.
3069
+ */
3070
+ outcome?: 'good' | 'scrap';
3071
+ /** 우선순위 — 나가지 않으면 미러가 배정 순서의 근거를 모른다(상태⊆이벤트). */
3072
+ priority?: number;
3073
+ startTime?: ISOTime;
3074
+ endTime?: ISOTime;
3075
+ /**
3076
+ * **실제로 들어가고 나온 자재** — ISA-95 `JobResponse.MaterialActual`(`OpMaterialActualType`).
3077
+ * 명세(계획)가 아니라 일어난 일이다. 투입 인원·설비와 같은 채널에 실어 실적을 한 곳에서 읽는다.
3078
+ */
3079
+ materialActual?: {
3080
+ definitionId: string;
3081
+ use: 'consumed' | 'produced';
3082
+ quantity: number;
3083
+ uom?: string;
3084
+ }[];
3085
+ }
3086
+ export interface EquipmentStatusDelta extends EffectivePeriod {
3087
+ moverId: string;
3088
+ kind: string;
3089
+ status: string;
3090
+ location?: string;
3091
+ /** 붙박인 자리(`EquipmentState.homeLocation`). 나가지 않으면 미러가 소속을 영영 모른다 — 상태⊆이벤트. */
3092
+ homeLocation?: string;
3093
+ /** 지금 붙어 있는 작업 — 사람·자산 델타와 같은 자리. 없으면 미러가 작업↔자원 연결을 모른다. */
3094
+ taskId?: string;
3095
+ /**
3096
+ * 계획 정지(정비·오프라인) — **커맨드로만 바뀌는 사실**이므로 델타로 실어 보내는 것이 맞다
3097
+ * (시각으로 바뀌는 판정과 다르다: 여기엔 항상 이벤트가 있다).
3098
+ *
3099
+ * 없어서 미러는 계획 정지를 **영영 몰랐다.** 그런데 적합성 하네스는 조용했다 — 대조가 값 있는 필드만
3100
+ * 세는데 아무도 정지 상태가 아닌 실행에서는 양쪽 다 비어 있었기 때문이다. 그래서 하네스가 실행 중간에
3101
+ * **조건을 일부러 일으키게** 고치고(`provoke`), 그 눈으로 이 구멍을 찾았다.
3102
+ */
3103
+ held?: boolean;
3104
+ motion?: EquipmentMotion;
3105
+ }
3106
+ /** 관측된 오더 라인(SKU 데맨드) — 실 시스템 오더는 품목 라인을 가짐. 이행 예측(남은 데맨드 재계획)에 필요. */
3107
+ export interface ObservedOrderLine {
3108
+ gtin: string;
3109
+ requested: number;
3110
+ fulfilled?: number;
3111
+ }
3112
+ export interface OrderStatusDelta {
3113
+ orderId: string;
3114
+ kind: string;
3115
+ status: string;
3116
+ requested: number;
3117
+ fulfilled: number;
3118
+ /**
3119
+ * **무엇을 만드나** — 상태의 `OrderState.gtin` 과 같은 값.
3120
+ *
3121
+ * 스냅샷에만 실으면 **미러가 이 필드를 못 채운다**(라이브는 이벤트로만 배운다). 같은 사실은
3122
+ * 델타·스냅샷·씨앗 **세 자리**에 함께 실어야 한다 — `allocated` 가 두 자리에만 있어서 웜스타트가
3123
+ * 잃었던 것과 같은 부류다(패리티 가드가 이것을 잡았다).
3124
+ */
3125
+ gtin?: string;
3126
+ /** 어느 레시피로 만드는가 — 품목만으로는 「무엇으로」가 남지 않는다(§`FlowOrder.recipeKey`). */
3127
+ recipeKey?: string;
3128
+ held?: boolean;
3129
+ /**
3130
+ * 오더 라인(SKU+수량) — 선택. 있으면 이행 예측이 "남은 데맨드(라인별 requested-fulfilled)를
3131
+ * 현재 재고로 어떻게 채우나"를 재계획할 수 있다. 없으면 top-level 카운트만(이행 예측 불가, 재고 예측만).
3132
+ */
3133
+ lines?: ObservedOrderLine[];
3134
+ /** 우선순위·예정 창 — 미러가 "늦었나" 를 판단할 재료. */
3135
+ priority?: number;
3136
+ startTime?: ISOTime;
3137
+ endTime?: ISOTime;
3138
+ /**
3139
+ * 이 오더가 **확보해 둔 물품들**과 그 **거래번호** — 진행 중인 할당.
3140
+ *
3141
+ * 델타가 나르지 않으면 웜스타트가 잃는다. 잃으면 되살아난 오더는 아무것도 안 잡은 것처럼
3142
+ * 보이고, 그 오더의 진행 중 작업이 완료될 때 계보(`TransformationEvent`)가 **입력 없이** 나간다 —
3143
+ * "무엇이 무엇으로 바뀌었나" 의 절반이 사라진다. 실제로 그렇게 되고 있었다.
3144
+ */
3145
+ allocated?: string[];
3146
+ bizTransaction?: string;
3147
+ /** 이행하는 계획 — 표준 `OperationsRequestID`(§`OrderState.operationsRequestId`). */
3148
+ operationsRequestId?: string;
3149
+ /**
3150
+ * 도메인이 **약속해 둔 자리와 시각창** — 어디로/언제 들이기로 했는가.
3151
+ *
3152
+ * ── 왜 사실에 실어야 하나 (2026-08-17) ─────────────────────────────────────
3153
+ * 야드 트윈이 재기동 뒤 **첫 틱에서 죽었다.** 어포인트먼트를 받을 때 도크 도어를 정해 두는데
3154
+ * 그것이 어느 사실에도 실리지 않아, 웜스타트가 오더는 되살리고 배정은 잃었다. 그리고 다음 틱에
3155
+ * 「그 도어의 점유」를 읽다 예외가 났다(`Cannot read properties of undefined`). 안전망이 그 트윈만
3156
+ * 세워 다른 트윈은 살았지만, 야드 트윈 셋이 부팅마다 죽는 상태였다.
3157
+ *
3158
+ * `allocated` 가 두 자리에만 있어 계보를 잃었던 것과 **같은 부류**다: 상태가 필요로 하는 것을
3159
+ * 사실이 갖고 있지 않으면 재기동을 넘지 못한다.
3160
+ */
3161
+ dockDoor?: string;
3162
+ windowStartMs?: number;
3163
+ }
3164
+ export declare const CMD: {
3165
+ readonly orderHold: "order.hold";
3166
+ readonly orderResume: "order.resume";
3167
+ readonly orderRelease: "order.release";
3168
+ readonly attentionAck: "attention.ack";
3169
+ readonly resourceHold: "resource.hold";
3170
+ readonly resourceResume: "resource.resume";
3171
+ readonly resourceDown: "resource.down";
3172
+ readonly resourceRepair: "resource.repair";
3173
+ readonly resourceResetMetrics: "resource.reset-metrics";
3174
+ readonly resourceAdd: "resource.add";
3175
+ };
3176
+ export type OperationalDelta = TaskStatusDelta | EquipmentStatusDelta | PersonStatusDelta | AssetStatusDelta | OrderStatusDelta;
3177
+ export type EventHandler = (e: CanonicalEnvelope) => void;
3178
+ export type Unsubscribe = () => void;
3179
+ /**
3180
+ * **저장된 트윈 모델을 읽는 단 하나의 입구.**
3181
+ *
3182
+ * `equipment` 로 개명하기 전에 저장된 트윈 모델은 `movers` 키를 갖고 있다(개명 시점 23개 인스턴스). (vocabulary-guard: allow — 읽기 호환 설명)
3183
+ * 저장물을 다시 쓰지 않고 **읽을 때 흡수**한다 — 마이그레이션은 되돌리기 어렵고, 읽기 호환은 값싸다.
3184
+ *
3185
+ * 규율 둘:
3186
+ * - 이 함수를 **거치지 않고** `def.equipment` 를 직접 읽는 코드를 두지 않는다. 하나라도 남으면
3187
+ * 그 경로에서만 옛 트윈 모델의 설비가 조용히 사라진다(빈 배열).
3188
+ * - **쓸 때는 새 이름만** 쓴다. 두 이름으로 쓰기 시작하면 저장물에 두 벌이 영구히 섞인다.
3189
+ *
3190
+ * 제거 시점: 저장된 트윈 모델이 모두 `equipment` 키로 바뀐 것이 확인되면(운영 데이터 점검 후) 이 함수는
3191
+ * 사라진다. 그때까지 남겨 두는 이유를 여기 적어 두는 것이 주석의 일이다.
3192
+ */
3193
+ /**
3194
+ * **저장된 트윈 모델의 자리를 읽는 단 하나의 입구.** `readBoardEquipment` 와 같은 규율.
3195
+ *
3196
+ * `nodes` → `locations` 개명(2026-08-01) 전에 저장된 트윈 모델은 `nodes` 키를 갖고 있다(개명 시점 23개). (vocabulary-guard: allow — 읽기 호환 설명)
3197
+ * 이 함수를 거치지 않고 `def.locations` 를 직접 읽는 코드를 두지 않는다 — 하나라도 남으면 그 경로에서만
3198
+ * 옛 트윈 모델의 자리가 조용히 사라진다(빈 배열 = 자리 없는 트윈 = 아무 일도 일어나지 않는다).
3199
+ */
3200
+ export declare function readBoardLocations(def: TwinModelDef | (Record<string, unknown> & {
3201
+ locations?: unknown;
3202
+ nodes?: unknown;
3203
+ })): TwinModelDef['locations'];
3204
+ export declare function readBoardEquipment(def: TwinModelDef | Record<string, unknown>): TwinModelDef['equipment'];
3205
+ /** 저장된 트윈 모델의 반복사용 자산 — 설비와 같은 정규화를 거친다. */
3206
+ export declare function readBoardAssets(def: TwinModelDef | Record<string, unknown>): NonNullable<TwinModelDef['assets']>;
3207
+ export interface TwinModelDef {
3208
+ /**
3209
+ * **이 트윈의 정체성 선언** — 팔레트·트레일러·문서의 식별자가 어디서 오나.
3210
+ *
3211
+ * ── 왜 모델에 있나 (2026-08-21) ──────────────────────────────────────────
3212
+ * 처음에는 `ProductionSpec.identity` 하나였다. 그때 선언 모드가 있는 커널이 MES 뿐이었기 때문이다.
3213
+ * 그런데 **정체성은 생산과 무관하다**: 창고는 팔레트(SSCC)를, 야드는 트레일러(GRAI)를 식별하는데
3214
+ * 그 둘은 아무것도 생산하지 않는다. 생산 선언에 두면 생산하지 않는 트윈이 정체성을 선언할 자리가 없고,
3215
+ * 실제로 그래서 두 커널이 프리픽스를 코드에 두고 있었다.
3216
+ *
3217
+ * 읽는 곳은 하나다(§`FlowEngine.identityDeclaration`). `ProductionSpec.identity` 는 **먼저 생긴
3218
+ * 자리**이고, 픽스처·호스트가 이 자리로 옮기면 사라진다.
3219
+ */
3220
+ identity?: IdentityDeclaration;
3221
+ /** parallelism = 동시 처리 수(LocationState.parallelism 참조). capacity 는 저장 용량. */
3222
+ locations: {
3223
+ id: string;
3224
+ type: string;
3225
+ capacity: number;
3226
+ parallelism?: number;
3227
+ parentId?: string;
3228
+ properties?: ResourceProperty[];
3229
+ testSpecificationIds?: TestSpecificationRefs;
3230
+ }[];
3231
+ /**
3232
+ * 설비(설비). mtbfMs/mttrMs 지정 시 확률적 고장 모델 참여(OEE Availability 손실). 미지정=고장 없음.
3233
+ * `window` 지정 시 그 시간대에만 일한다(교대·가동시간) — 미지정이면 24시간 가용(기존 거동).
3234
+ */
3235
+ equipment: (EffectivePeriod & {
3236
+ id: string;
3237
+ kind: string;
3238
+ homeLocation: string;
3239
+ identity?: string;
3240
+ mtbfMs?: number;
3241
+ mttrMs?: number;
3242
+ window?: {
3243
+ startHour: number;
3244
+ endHour: number;
3245
+ };
3246
+ workCalendar?: WorkCalendarEntry[];
3247
+ properties?: ResourceProperty[];
3248
+ testSpecificationIds?: TestSpecificationRefs;
3249
+ testResults?: TestResult[];
3250
+ })[];
3251
+ /**
3252
+ * 사람 — ISA-95 `Person`. `personnelClasses` 로 **속한 등급들**을 밝히고(복수가 표준), `window` 로
3253
+ * 교대를 선언한다.
3254
+ * 선언하지 않으면 인원 제약이 없는 트윈이다(기존 거동).
3255
+ */
3256
+ persons?: (EffectivePeriod & {
3257
+ id: string;
3258
+ personnelClassIds?: string[];
3259
+ window?: {
3260
+ startHour: number;
3261
+ endHour: number;
3262
+ };
3263
+ workCalendar?: WorkCalendarEntry[];
3264
+ homeLocation?: string;
3265
+ properties?: ResourceProperty[];
3266
+ testSpecificationIds?: TestSpecificationRefs;
3267
+ testResults?: TestResult[];
3268
+ })[];
3269
+ /** 물리 자산(반복사용) — ISA-95 `PhysicalAsset` / GS1 `GRAI`. 선언하지 않으면 자산 제약이 없다. */
3270
+ assets?: (EffectivePeriod & {
3271
+ id: string;
3272
+ assetClassIds?: string[];
3273
+ homeLocation?: string;
3274
+ properties?: ResourceProperty[];
3275
+ testSpecificationIds?: TestSpecificationRefs;
3276
+ testResults?: TestResult[];
3277
+ })[];
3278
+ /**
3279
+ * 이 트윈의 **시각 해석 기준**(UTC 로부터의 분). 근무 캘린더의 `HH:MM` 이 어느 기준인지 정한다.
3280
+ *
3281
+ * 예: Rosarito(UTC−7) = `-420`. **선언하지 않으면 UTC**(0)로 읽는다 — 조용히 현지 시각으로
3282
+ * 가정하지 않는다. 테넌트 시간대는 호스트가 안다(`Domain.timezone`) — 그것을 오프셋으로 풀어 준다.
3283
+ *
3284
+ * 일광절약시간은 고정 오프셋으로 따라갈 수 없다. 긴 지평선의 정확한 답은 호스트가 표준대로
3285
+ * **절대 구간**(`StartDateTime`/`FinishDateTime`)을 계산해 넣는 것이다.
3286
+ */
3287
+ utcOffsetMinutes?: number;
3288
+ /**
3289
+ * **시험 명세** — 표준 `TestSpecification`. 자원의 `testSpecificationIds` 가 가리키는 대상.
3290
+ *
3291
+ * 선언하지 않아도 트윈은 동작한다 — 그때 그 참조는 **가리킬 것이 없다**(화면이 끊어진 참조로 센다).
3292
+ * 커널은 이 값으로 자격을 판정하지 않는다: 판정에는 **결과**가 필요하고 결과는 아직 모델에 없다.
3293
+ * 없는 것으로 판정하면 자격자가 전부 사라져 라인이 영구히 굶는다.
3294
+ */
3295
+ testSpecifications?: TestSpecification[];
3296
+ /**
3297
+ * **등급 정의** — 표준 `PersonnelClass` · `EquipmentClass` · `PhysicalAssetClass`.
3298
+ *
3299
+ * 선언하지 않아도 트윈은 동작한다(소속 문자열 그대로 판정). 선언하면 **상속과 유효기간**이 살아난다 —
3300
+ * "생산직 2명" 요구를 "용접 자격자" 가 만족하고, 만료된 자격은 배정되지 않는다.
3301
+ */
3302
+ /**
3303
+ * **품목 정의**(표준 `MaterialDefinition`)와 **품목 등급**(`MaterialClass`).
3304
+ *
3305
+ * 선언하지 않아도 트윈은 동작한다 — 그때는 단위 환산을 할 수 없고, `quantityIn` 이 **없다고 답한다**
3306
+ * (계수를 모르는데 값을 만들면 그 뒤 모든 계산이 거짓 위에 선다).
3307
+ */
3308
+ materialDefinitions?: MaterialDefinition[];
3309
+ materialClasses?: ResourceClassDef[];
3310
+ personnelClasses?: ResourceClassDef[];
3311
+ equipmentClasses?: ResourceClassDef[];
3312
+ assetClasses?: ResourceClassDef[];
3313
+ /**
3314
+ * **생산 선언** — 이 트윈이 무엇을 어떻게 만드는가(ISA-95 `OperationsSegment` + BOM).
3315
+ *
3316
+ * ── 이름을 `productionSpec` 으로 정한 이유 (2026-08-05, 되돌리지 말 것) ─────────────────────────
3317
+ * 이 자리는 예전에 **`mesSpec`** 이라는 이름으로, 그것도 **계약에 선언되지 않은 채**
3318
+ * (`(board as any).mesSpec`) 타고 있었다.
3319
+ *
3320
+ * 그 이름의 유래는 자재가 아니라 **소비자**다: `MesKernel` 의 생성자 인자 이름이
3321
+ * `mesSpec: MesDefinitionSpec` 이고(정의-구동 모드를 MES 에만 도입한 커밋 `ce4fbeb`), 호스트가
3322
+ * 트윈 모델에 얹을 때 그 인자 이름을 그대로 가져왔다. 소비자가 하나일 때는 어색하지 않았다.
3323
+ *
3324
+ * `materialSpec` 도 답이 아니다. ① 담긴 것이 자재가 아니다 — 타입·오퍼레이션(소요·변동·모수·
3325
+ * 인원/설비/자산/자재 명세)·라우트·레시피이고 자재는 그중 한 항목의 한 필드다. ② 그 이름은 이미
3326
+ * 표준 자리로 쓰인다 — `OperationDef.materialSpecification` 이 ISA-95 `OpMaterialSpecificationType`
3327
+ * 1:1 이다. 보드 수준에서 같은 이름을 쓰면 "어느 쪽 자재 명세냐" 가 매번 헷갈린다.
3328
+ *
3329
+ * `productionSpec` = ISA-95 의 생산 영역 어휘이고, 담긴 것("이 트윈이 무엇을 어떻게 만드는가")
3330
+ * 그대로다. 특정 시스템(MES/WMS)에도, 특정 자원(자재)에도 기울지 않는다.
3331
+ *
3332
+ * 이름이 MES 였던 결과로 두 가지가 굳었다.
3333
+ *
3334
+ * ① **일반 기제에 한 시스템 이름이 붙었다.** 담고 있는 것은 ISA-95 `OperationsSegment`(소요·모수·
3335
+ * 자재 명세)와 BOM 이고, 그것은 MES 만의 것이 아니다 — 창고의 유통가공(키팅·세트조립)도 같은
3336
+ * "자재를 소비해 자재를 산출하는 공정" 이다. 이름이 MES 였기 때문에 **WMS 트윈은 이 선언을 실을
3337
+ * 생각조차 하지 못했고**, 유통가공 창고 템플릿은 BOM 을 설명 문서(`detail`)에만 적어 둔 채
3338
+ * 키트를 만들지 못하는 트윈을 만들어 냈다(랙이 꽉 차고 출고가 0건이었다).
3339
+ * ② 계약에 없으니 **아무도 이 자리를 발견할 수 없었다.** 타입이 말해 주지 않는 필드는 없는 필드다.
3340
+ *
3341
+ * 그래서 표준 어휘를 따르는 일반 이름으로 정식 선언한다. `mesSpec` 은 기존 트윈이 그대로 돌도록
3342
+ * **읽기 호환으로만** 남긴다(새로 쓰지 않는다).
3343
+ *
3344
+ * ── 시스템별 생산의 경계 (여기서도 되돌리지 말 것) ────────────────────────────────────────────
3345
+ * MES 의 생산은 **직렬번호·수율·생산오더 연결**을 갖는 레시피 경로다. 코어의 일반 자재 명세
3346
+ * (`use: 'produced'`)는 **비직렬 클래스+수량**이라 그 셋을 표현하지 못한다 — 그래서 둘을 합치지
3347
+ * 않는다. 겹치면 같은 산출이 두 번 생기므로 MES 커널이 기동 시점에 거부한다(`assertNoDoubleProduction`).
3348
+ * 유통가공(VAS)은 비직렬 클래스+수량이 맞는 모양이므로 코어의 일반 경로를 쓴다.
3349
+ */
3350
+ productionSpec?: ProductionSpec;
3351
+ /**
3352
+ * 공정 명세 — **ISA-95 `OperationsSegment`.** 이 현장에서 각 작업이 어디서·무엇으로·얼마나 걸리나.
3353
+ *
3354
+ * ── 왜 이제야 선언하나 (2026-08-08) ──────────────────────────────────────────
3355
+ * 이 자리는 **이미 쓰이고 있었다.** 마스터 인제스트가 `board.operations` 로 통과시키고 호스트가
3356
+ * `applyOperations` 로 커널에 싣는다. 그런데 `TwinModelDef` 는 그것을 말한 적이 없다.
3357
+ *
3358
+ * 바로 위 `productionSpec` 이 겪은 것과 **같은 일**이다 — "계약에 없으니 아무도 이 자리를 발견할
3359
+ * 수 없었다. 타입이 말해 주지 않는 필드는 없는 필드다." 그때 배운 것을 여기 적용한다.
3360
+ *
3361
+ * 소비 경로가 둘인 것은 그대로 둔다(여기와 `productionSpec.definition.operations`) — 후자는
3362
+ * 라우트의 단계를 해소할 때 읽힌다. 집을 하나로 모으는 것은 별건이고, 그 전에 **선언부터** 한다.
3363
+ */
3364
+ operations?: import('./domain-definition.ts').OperationDef[];
3365
+ }
3366
+ /**
3367
+ * 생산 선언 — 도메인 정의(ISA-95 operations + BOM)와, 그 추상 자재 키를 실제 GS1 식별자로 잇는 바인딩.
3368
+ *
3369
+ * 정의는 **gtin 을 모른다**(어느 회사의 물건인지는 현장의 사실이다). 그래서 키→item reference 바인딩과
3370
+ * 회사 프리픽스를 여기서 받아 `urn:epc:idpat:sgtin:<prefix>.<ref>.*` 를 만든다.
3371
+ */
3372
+ export interface ProductionSpec {
3373
+ /** 무엇이 있고 어떻게 흐르나 — `DomainDefinition`(자기 타입, zero-dep). */
3374
+ definition: import('./domain-definition.ts').DomainDefinition;
3375
+ /**
3376
+ * 자재 키 → **그 자재의 클래스 식별자를 정하는 값.**
3377
+ *
3378
+ * 두 모양을 받는다. `companyPrefix` 가 있으면 값은 GS1 item reference 이고 클래스는
3379
+ * `urn:epc:idpat:sgtin:<prefix>.<ref>.*` 로 조립된다. 프리픽스가 없으면 값은 **이미 완성된 클래스
3380
+ * URI** 이고(CBV 2.0 §8.3.4 `.../class/ObjClassid`) 커널은 그것을 그대로 쓴다 — 조립하면 값을 망친다.
3381
+ *
3382
+ * ── 같은 자재를 가리키는 이름은 하나다 ──────────────────────────────────────
3383
+ * 공정 명세의 `materialSpecification.materialDefinition` 도 **이 값에서 나온 클래스 식별자**여야 한다.
3384
+ * 두 자리가 같은 자재를 다른 이름으로 부르면, 커널은 그것이 같은 자재인지 알 길이 없다 — 같은 산출을
3385
+ * 두 번 만드는 것을 막는 기동 검사가 그때 조용히 열린다(§`assertNoDoubleProduction`). 옮길 수 없는
3386
+ * 식별자는 기동에서 말한다.
3387
+ */
3388
+ binding?: Record<string, string>;
3389
+ companyPrefix?: string;
3390
+ /** 쓸 레시피 키(미지정 시 첫 레시피) — 직렬 생산 경로(MES)만 쓴다. */
3391
+ recipeKey?: string;
3392
+ /** 이 트윈의 **정체성이 어디서 오나** — §IdentityDeclaration. 없으면 근거는 선언 형태에서 파생된다. */
3393
+ identity?: IdentityDeclaration;
3394
+ }
3395
+ /**
3396
+ * 정체성 선언 — **이 트윈이 자기 것이라 말하는 이름공간.**
3397
+ *
3398
+ * ── 왜 필요한가 (2026-08-20) ────────────────────────────────────────────────
3399
+ * 커널이 품목 식별자를 지어내고 있었다(`sgtinUri(prefix, 'WIP', …)` · 상수 프리픽스). 그 압력의 출처는
3400
+ * 우리 검증기였다: 수량 리스트의 `epcClass` 에 EPC 형식을 강제했으므로, GS1 프리픽스가 없는 현장은
3401
+ * **통과할 방법이 없었다.** 그래서 지어냈다.
3402
+ *
3403
+ * 원문을 보니 그 강제가 표준의 요구가 아니었다: EPCIS 2.0 §6.4 는 「**소유 권한이 있는** URI」를 요구하고,
3404
+ * CBV 2.0 은 클래스 식별에 일반 HTTP URL 을 명시한다. 즉 **도메인만 있으면 GS1 발급 없이도 정합**이다.
3405
+ * 진입장벽은 우리가 만든 것이었다.
3406
+ *
3407
+ * 그래서 이름공간을 **현장이 선언한다.** 검증은 두 조건의 곱이다: 소유 권한이 있는 형태인가(표준) ∧
3408
+ * 이 트윈이 자기 것이라 선언한 범위인가(모델). 하나만 요구하면 각각 아무 도메인이나 통과하거나,
3409
+ * 소유 권한 없는 스킴이 통과한다.
3410
+ */
3411
+ export interface IdentityDeclaration {
3412
+ /**
3413
+ * 이 트윈의 식별자가 속하는 이름공간들 — 예: `https://chef.example.com/product/` ·
3414
+ * `urn:epc:idpat:sgtin:0952000.` · `urn:oid:1.3.6.1.4.1.<PEN>.`
3415
+ *
3416
+ * 커널은 이 목록을 **정하지 않고 묻기만** 한다. 비어 있으면 선언이 없는 것이고, 없는 것을 채우지 않는다.
3417
+ *
3418
+ * **이름공간은 소속만 정한다.** 값의 모양(HTTP URL 의 `/class/` 표지 · URN 의 `:class:`)은 표준이 정하고
3419
+ * `classIdentifierViolation()` 이 판정한다 — 이름공간에 넣었다고 통과하는 것이 아니다.
3420
+ *
3421
+ * **선택이다.** GDTI 문서 타입만 선언하는 현장은 이름공간을 갖지 않는다(§`documentTypes`). 없는 것을
3422
+ * 억지로 적게 하면 그 현장은 빈 목록이나 거짓 이름공간을 쓰게 되고, 그때 근거 판정이 함께 거짓이 된다.
3423
+ */
3424
+ namespaces?: string[];
3425
+ /**
3426
+ * 이 이름공간이 **발급받은 GS1 키**라는 주장.
3427
+ *
3428
+ * **주장이다 — 우리는 검증하지 않는다.** GEPIR 조회를 하지 않으므로 우리가 아는 것은 「모델이 그렇게
3429
+ * 말했다」까지다. 이 칸이 없으면 근거는 `declared` 이고, 그것도 정직한 값이다(형식은 GS1 이어도 출처는
3430
+ * 모델이다 — 실제로 우리 픽스처가 GS1 이 버린 예제 프리픽스를 그 자리에 두고 있었다).
3431
+ */
3432
+ issuedClaim?: boolean;
3433
+ /**
3434
+ * **거래 문서 식별자**(작업지시 등)를 만들 때 쓰는 GDTI **문서 타입** — 종류별로 선언한다.
3435
+ *
3436
+ * ── 왜 이 칸이 필요한가 (2026-08-21) ─────────────────────────────────────
3437
+ * GDTI 는 `회사 프리픽스 + 문서 타입 + 일련번호`이고, **문서 타입은 GS1 이 공표하는 목록이 아니다** —
3438
+ * 프리픽스를 배정받은 회사가 자기 번호 용량에서 정한다. 그런데 커널이 `'403'`(작업지시)·`'401'`·
3439
+ * `'402'`·`'404'` 를 스스로 정해 저널에 영구히 기록하고 있었다. 회사의 배정 권한을 커널이 대신
3440
+ * 행사한 것이고, 프리픽스 날조와 같은 종류의 결함이다.
3441
+ *
3442
+ * **선언하지 않아도 된다.** 표준은 거래 문서 식별자에 GDTI 만 허용하지 않는다 — 선언된 이름공간
3443
+ * 아래 `.../bt/<id>`(CBV §8.5.5) 또는 `urn:<이름공간>:**:bt:<id>`(§8.5.4)가 정합이고, 그쪽이
3444
+ * **회사가 도메인만 있으면 되는** 더 쉬운 길이다. 이 칸은 GDTI 를 쓰는 현장을 위해 열어 둔다.
3445
+ *
3446
+ * 키는 문서의 종류(예: `workorder`), 값은 그 현장이 배정한 문서 타입이다.
3447
+ */
3448
+ documentTypes?: Record<string, string>;
3449
+ /**
3450
+ * **이 현장이 배정받은 GS1 회사 프리픽스** — GS1 키(SSCC·GRAI·GDTI)를 쓰는 현장이 선언한다.
3451
+ *
3452
+ * 이 자리가 없으면 GS1 키를 만들 수 없다. 그것이 막힘이 아니라 **정직**이다: 프리픽스는 GS1 이
3453
+ * 회사에 배정하는 것이고, 커널이 값을 고르면 저널에 남의 번호가 영구히 남는다.
3454
+ *
3455
+ * 프리픽스가 없는 현장은 `namespaces` 로 간다 — 도메인만 있으면 표준 정합이다(CBV §8.2.4·§8.5.5).
3456
+ * 그쪽이 진입장벽이 낮다.
3457
+ *
3458
+ * 여기 있다는 것이 **발급받았다는 증명은 아니다** — 그 주장은 `issuedClaim` 이 따로 말하고, 우리는
3459
+ * 확인하지 않는다(§`IdentityGroundingView`).
3460
+ */
3461
+ companyPrefix?: string;
3462
+ }
3463
+ /**
3464
+ * 정체성의 **근거** — 값이 어디서 왔나. 「우리가 구현했나」와 **다른 축**이다.
3465
+ *
3466
+ * 커버리지 세 축(구조·거동·표면)은 모두 `full` 이면서 그 값이 남의 번호일 수 있다. 그래서 이 축이 없으면
3467
+ * 「정체성 지원 완료」가 참인 동시에 저널에 위조가 쌓인다.
3468
+ *
3469
+ * ── 값마다 **신뢰도가 다르다** ──────────────────────────────────────────────
3470
+ * · `issued` — 모델이 발급받았다고 **주장**한 것. 우리는 확인하지 않는다(가장 약한 값).
3471
+ * · `declared` — 모델이 선언한 이름공간에서 왔다. 이것은 **확실히 안다**(선언이 근거다).
3472
+ * · `fabricated` — 코드 리터럴에서 왔다. 이것도 확실히 안다 — 그리고 **릴리즈를 통과해선 안 된다.**
3473
+ *
3474
+ * 어댑터의 `grounding`(`vendor-doc`·`standard`·`facsimile`)과 낱말은 겹치지만 **값 집합이 다르다**:
3475
+ * 저쪽은 「우리 매핑의 근거가 무엇인가」, 이쪽은 「식별자가 어디서 왔는가」다. 서로 대입하지 말 것.
3476
+ *
3477
+ * ── 표준의 권고는 여전히 EPC URI 다 ────────────────────────────────────────
3478
+ * CBV 2.0 §8.3.3·§8.3.4 는 둘 다 이렇게 말한다: *"both CBV-Compliant and CBV-Compatible documents **SHOULD**
3479
+ * use the EPC URI form unless there is a **strong** reason to do otherwise."* 즉 「사내 식별자도 표준이다」는
3480
+ * 맞지만, 표준이 **권하는 것은 GS1 키**다. 「GS1 프리픽스가 없다」가 그 strong reason 에 해당한다.
3481
+ * 이 축은 GS1 을 권하지 않으려고 있는 것이 아니라, **없는 현장을 막지 않으려고** 있다.
3482
+ *
3483
+ * ── 이름공간에 든다고 통과가 아니다 ────────────────────────────────────────
3484
+ * 판정은 두 겹이고 **서로 다른 것을 본다**:
3485
+ * · (a) **모양** — 소유 권한을 말할 수 있는 형태인가. HTTP(S) URL 은 `…/class/<Objclassid>` 표지가
3486
+ * **필수**이고(CBV §8.3.4), URN 은 `…:class:<Objclassid>` 가 필수다(§8.3.3). 즉 이름공간이
3487
+ * `https://chef.example.com` 이어도 값이 `…/product/<uuid>` 면 **부적합**하다 — `…/class/<uuid>` 여야 한다.
3488
+ * · (b) **소속** — 그 값이 `namespaces` 가 말한 범위 안인가.
3489
+ *
3490
+ * (a) 의 규칙은 한 곳에만 있다: `epcis.ts` 의 `classIdentifierViolation()`. 소비처는 그것을 **부르고**,
3491
+ * 규칙을 다시 적지 않는다(두 벌이 되면 한쪽만 고쳐진다).
3492
+ */
3493
+ export type IdentityGrounding = 'issued' | 'declared' | 'fabricated';
3494
+ /** 근거 판정 — 값과, 왜 그 값인지, 그리고 검증기가 쓸 이름공간. */
3495
+ export interface IdentityGroundingView {
3496
+ grounding: IdentityGrounding;
3497
+ /**
3498
+ * 그 판정의 근거 자리 — 화면·게이트가 「왜」를 말할 수 있게.
3499
+ * · `claimed-issued` — 모델이 발급을 주장했다
3500
+ * · `declared-namespace` — 모델이 이름공간을 선언했다
3501
+ * · `gs1-shaped` — `companyPrefix`+`binding` 은 있으나 발급 주장은 없다(형식만 GS1)
3502
+ * · `kernel-constant` — 선언이 없어 커널 상수로 돈다(레거시 내장 시나리오)
3503
+ */
3504
+ basis: 'claimed-issued' | 'declared-namespace' | 'gs1-shaped' | 'kernel-constant';
3505
+ /**
3506
+ * 이 트윈이 자기 것이라 선언한 이름공간들 — 검증기의 두 번째 조건이 이것을 쓴다.
3507
+ *
3508
+ * 선언이 없는 레거시 트윈에는 **커널 상수에서 파생된** 값이 들어온다. 그것이 통과를 만들어 주지만
3509
+ * 정직성은 `grounding: 'fabricated'` 가 지킨다 — 릴리즈 게이트가 그 값으로 막는다. 레거시 경로의
3510
+ * 사건을 여기서 거절하면 그 트윈은 아무 사실도 남기지 못하는데, 그건 지금 고치는 문제가 아니다.
3511
+ */
3512
+ namespaces: string[];
3513
+ }
3514
+ /**
3515
+ * 선언에서 근거를 **파생한다** — 새 사실을 만들지 않는다.
3516
+ *
3517
+ * `companyPrefix` 가 있다는 사실을 `issued` 로 읽지 **않는다**: 그 값이 이 회사에 발급되었다는 뜻이
3518
+ * 아니기 때문이다(우리 픽스처가 반례다 — GS1 이 2022 년에 버린 예제 프리픽스 `0614141` 이 그 자리에
3519
+ * 있었다). 발급은 모델이 **따로 말해야** 하고, 그것도 주장이다.
3520
+ */
3521
+ export declare function identityGroundingOf(spec: ProductionSpec | undefined, kernelConstantPrefix?: string): IdentityGroundingView;
3522
+ /**
3523
+ * 공장을 전환한 결과 — **몇 개가 늘고 몇 개가 사라졌나.**
3524
+ *
3525
+ * 돌려주지 않으면 조용히 사라진다. 자리 하나가 없어진 것을 사용자가 화면의 수를 세어 눈치채기를
3526
+ * 기대할 수는 없다(특히 자리가 수천인 현장에서). 그래서 구조 전환은 **말없이 성공하지 않는다.**
3527
+ */
3528
+ export interface StructureShift {
3529
+ locationsAdded: number;
3530
+ locationsDropped: number;
3531
+ equipmentDropped: number;
3532
+ personsDropped: number;
3533
+ assetsDropped: number;
3534
+ }
3535
+ export interface TwinKernel {
3536
+ loadTwinModel(def: TwinModelDef): void;
3537
+ /**
3538
+ * 돌면서 공장을 전환한다 — 관측 구동(미러)에서만. 시뮬레이션은 거절한다(멈추고 다시 세운다).
3539
+ * 관측으로 알게 된 자리는 새 선언에 없어도 남고, 선언에서 사라진 자원은 버리되 수를 돌려준다.
3540
+ */
3541
+ adoptStructure(def: TwinModelDef): StructureShift;
3542
+ /** 이 커널이 관측으로 구동됨을 선언한다 — 첫 이벤트가 오기 전에도 그렇다. */
3543
+ observe(): void;
3544
+ /**
3545
+ * 시각 기준점을 세운다 — 이 트윈의 「지금」은 `기준점 + 경과`다.
3546
+ *
3547
+ * 세우는 쪽이 기동 순간의 실제 시각을 준다. 주지 않으면 커널의 기본 기준점을 쓰는데, 그러면 시뮬
3548
+ * 트윈의 저널 시각이 재기동마다 되감겨 「언제 있었던 일인가」를 되짚을 수 없다.
3549
+ * **기동 직후에만** 부른다 — 시계가 흐른 뒤에 옮기면 그전에 낸 사실들과 시간축이 어긋난다.
3550
+ */
3551
+ setClockOrigin?(originMs: number): void;
3552
+ getSnapshot(): StateSnapshot;
3553
+ onEvent(handler: EventHandler): Unsubscribe;
3554
+ dispatch(cmd: Command): CommandAck;
3555
+ readonly scenario: ScenarioControl;
3556
+ tick(dtMs: number): void;
3557
+ }