@operato/twin-kernel 0.2.3 → 0.4.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.
- package/README.md +46 -5
- package/dist/capability.js +2 -2
- package/dist/capacity.d.ts +99 -0
- package/dist/capacity.js +172 -0
- package/dist/contract.d.ts +836 -45
- package/dist/contract.js +519 -7
- package/dist/divergence.d.ts +1 -1
- package/dist/divergence.js +8 -5
- package/dist/domain-catalog.d.ts +4 -4
- package/dist/domain-catalog.js +4 -4
- package/dist/domain-definition.d.ts +50 -5
- package/dist/domain-definition.js +6 -6
- package/dist/epcis.d.ts +12 -0
- package/dist/epcis.js +12 -0
- package/dist/event-journal.d.ts +31 -1
- package/dist/event-journal.js +27 -1
- package/dist/flow-engine.d.ts +323 -36
- package/dist/flow-engine.js +868 -164
- package/dist/forecast.d.ts +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/job-response.d.ts +59 -0
- package/dist/job-response.js +56 -0
- package/dist/kernel.d.ts +5 -0
- package/dist/kernel.js +31 -11
- package/dist/mes-kernel.d.ts +28 -0
- package/dist/mes-kernel.js +83 -29
- package/dist/mes-profile.d.ts +2 -2
- package/dist/mes-profile.js +11 -11
- package/dist/observed-reducer.d.ts +68 -7
- package/dist/observed-reducer.js +248 -41
- package/dist/task-fold.d.ts +2 -2
- package/dist/task-fold.js +4 -4
- package/dist/vocabulary.d.ts +19 -0
- package/dist/vocabulary.js +66 -0
- package/dist/wms-profile.d.ts +2 -2
- package/dist/wms-profile.js +6 -6
- package/dist/yms-kernel.js +36 -15
- package/dist/yms-profile.d.ts +2 -2
- package/dist/yms-profile.js +6 -6
- package/dist-cjs/index.cjs +1618 -270
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -11,18 +11,492 @@ export type TaskStatus = 'created' | 'assigned' | 'in-progress' | 'completed';
|
|
|
11
11
|
/**
|
|
12
12
|
* 자리의 상태 — **포화도에서 파생한다.** 저장하는 값이 아니다.
|
|
13
13
|
*
|
|
14
|
-
* 예전에는 시뮬이 `'idle'` 로 두고 한 번도 바꾸지 않았고(변경 지점 0), 미러에는
|
|
14
|
+
* 예전에는 시뮬이 `'idle'` 로 두고 한 번도 바꾸지 않았고(변경 지점 0), 미러에는 자리 상태 채널이
|
|
15
15
|
* 없어 비어 있었다. 화면은 그 값을 그대로 보여 주고 있었다 — **정보처럼 보이는데 정보가 아니었다.**
|
|
16
16
|
*
|
|
17
17
|
* 문턱은 병목 주목(`deriveAttentions`)이 쓰는 것과 **같다**: 90% 이상이면 임박, 100% 이상이면 포화.
|
|
18
18
|
* 규칙이 둘이면 화면과 주목이 다른 말을 한다. 용량을 모르면 상태도 모른다(undefined — 꾸미지 않는다).
|
|
19
19
|
*/
|
|
20
|
-
export declare const
|
|
21
|
-
export declare function
|
|
20
|
+
export declare const LOCATION_SATURATION_NEAR = 0.9;
|
|
21
|
+
export declare function locationStatusOf(n: {
|
|
22
22
|
occupancy?: number;
|
|
23
23
|
capacity?: number;
|
|
24
24
|
}): 'available' | 'near-full' | 'full' | undefined;
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* 설비 계층 단계 — **ISA-95 표준 어휘.** 1차 출처: B2MML `B2MML-Common.xsd` /
|
|
27
|
+
* `EquipmentLevel1Type` 열거값 + `EquipmentLevelType` 주석("role based equipment hierarchy level
|
|
28
|
+
* as defined in ISA 95").
|
|
29
|
+
*
|
|
30
|
+
* 우리가 단의 이름을 발명하지 않는다. "라인" 은 `ProductionLine`, "존" 은 `StorageZone` 으로
|
|
31
|
+
* 표준이 이미 정해 뒀다. 발명하면 그 순간 방언이 되고, 연동 상대와 매핑 표가 필요해진다.
|
|
32
|
+
*
|
|
33
|
+
* **`Other` 는 탈출구다** — 표준도 열거값 밖을 인정한다(`OtherValue` 속성). 억지로 끼워 맞추는 대신
|
|
34
|
+
* `Other` 로 두고 현장의 낱말은 `type` 에 남긴다.
|
|
35
|
+
*
|
|
36
|
+
* `StorageZone`·`StorageUnit` 도 이 계층 안에 있다. 다만 **자리 자체는 다른 축**이다 —
|
|
37
|
+
* ISA-95 는 `OperationalLocation`("자원이 놓이거나 놓일 것으로 예상되는 논리적·물리적 장소",
|
|
38
|
+
* `B2MML-OperationalLocation.xsd`)을 별도 스키마로 두고, `Equipment` 가 자기 위치를 그것으로 가리킨다.
|
|
39
|
+
* 우리 `locations` 가 그 개념이다(2026-08-01 개명 — `plans/isa95-coverage.md` §3-1).
|
|
40
|
+
*/
|
|
41
|
+
export declare const EQUIPMENT_LEVEL: readonly ["Enterprise", "Site", "Area", "ProcessCell", "Unit", "ProductionLine", "WorkCell", "ProductionUnit", "StorageZone", "StorageUnit", "WorkCenter", "WorkUnit", "EquipmentModule", "ControlModule", "Other"];
|
|
42
|
+
export type EquipmentLevel = (typeof EQUIPMENT_LEVEL)[number];
|
|
43
|
+
/** 표준 열거값인지 — 상류에서 들어온 값을 조용히 통과시키지 않고 확인하는 용도. */
|
|
44
|
+
export declare function isEquipmentLevel(v: unknown): v is EquipmentLevel;
|
|
45
|
+
/**
|
|
46
|
+
* 계층 질의 — **사슬을 걷는 규칙 한 벌.**
|
|
47
|
+
*
|
|
48
|
+
* 왜 커널이 내는가: 자리는 자리 밑에 들 수 있고(라인 ⊃ 스테이션), 설비는 자리에 붙박인다. 그래서
|
|
49
|
+
* "이 라인의 처리량" 같은 질문은 사슬을 걷어야 답이 나온다. 소비처(화면·집계·AI)가 각자 걷게 두면
|
|
50
|
+
* 규칙이 여러 벌이 되고, 한 홉만 보는 코드가 하나 남는 순간 **그 아래가 집계에서 조용히 빠진다.**
|
|
51
|
+
*
|
|
52
|
+
* `locationStatusOf` 와 같은 자리에 두는 이유도 같다 — 파생은 한 곳에서만 한다.
|
|
53
|
+
*/
|
|
54
|
+
export interface Hierarchy {
|
|
55
|
+
/** 바로 아래 자리들. */
|
|
56
|
+
childrenOf(locationId: string): string[];
|
|
57
|
+
/** 상위 사슬 — 가까운 쪽부터. 마지막 원소는 자리가 아닐 수 있다(=구역). 자기 자신은 넣지 않는다. */
|
|
58
|
+
ancestorsOf(locationId: string): string[];
|
|
59
|
+
/**
|
|
60
|
+
* 사슬의 끝 — **자리가 아닌 최상위 소속(=구역).** 자리만으로 사슬이 끝나면(구역 미선언) `undefined`.
|
|
61
|
+
* 모름을 빈 문자열이나 자기 id 로 **꾸미지 않는다** — 구역 미상은 구역 0 이 아니다.
|
|
62
|
+
*/
|
|
63
|
+
rollupOf(locationId: string): string | undefined;
|
|
64
|
+
/** 아래 전부(재귀). 자기 자신은 넣지 않는다. */
|
|
65
|
+
descendantsOf(locationId: string): string[];
|
|
66
|
+
/**
|
|
67
|
+
* 사슬을 올라가다 만나는 **그 단계의 가장 가까운 상위 자리** — "이 스테이션이 속한 라인" 은
|
|
68
|
+
* `ancestorOfLevel(st, 'ProductionLine')`. **이것이 표준 축이고 기본 질의다.**
|
|
69
|
+
*
|
|
70
|
+
* 없으면 `undefined` — 그 현장에 그 단계가 선언되지 않았다는 뜻이고, 아무 자리도 대신 내놓지 않는다.
|
|
71
|
+
*/
|
|
72
|
+
ancestorOfLevel(locationId: string, level: EquipmentLevel): string | undefined;
|
|
73
|
+
/**
|
|
74
|
+
* 같은 질의를 **현장의 낱말**(`type`)로 — 단계를 아직 선언하지 않은 데이터를 위한 보조 경로다.
|
|
75
|
+
*
|
|
76
|
+
* 표준 단계가 있으면 `ancestorOfLevel` 을 쓴다. 이것은 마스터가 `level` 을 실어 주기 전까지의
|
|
77
|
+
* 임시 다리이고, 여기에 새 어휘를 쌓지 않는다(쌓으면 그게 방언이 된다).
|
|
78
|
+
*/
|
|
79
|
+
ancestorOfType(locationId: string, type: string): string | undefined;
|
|
80
|
+
/**
|
|
81
|
+
* 이 자리에 **붙박인** 설비(`homeLocation` 기준) — 워크센터가 자리이자 설비인 경우의 이음.
|
|
82
|
+
* `deep` 이면 아래 자리들의 설비까지("이 라인의 설비").
|
|
83
|
+
*
|
|
84
|
+
* 지금 그 자리에 **와 있는** 설비(`location` 기준)와 다른 질문이다 — 지게차는 어디에나 와 있을 수
|
|
85
|
+
* 있지만 어디에도 붙박이지 않는다. 둘을 한 함수로 뭉개면 소속과 현재 위치가 섞인다.
|
|
86
|
+
*/
|
|
87
|
+
equipmentOf(locationId: string, opts?: {
|
|
88
|
+
deep?: boolean;
|
|
89
|
+
}): string[];
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 계층 색인을 만든다. **순환은 만들 때 잡고 던진다** — 렌더 도중에 터지는 대신 여기서 한 번에.
|
|
93
|
+
* 순환을 조용히 잘라 내면 롤업이 틀린 값을 내고, 그건 이 함수가 막으려는 바로 그 실패다.
|
|
94
|
+
*/
|
|
95
|
+
export declare function hierarchyOf(s: {
|
|
96
|
+
locations: readonly {
|
|
97
|
+
id: string;
|
|
98
|
+
type?: string;
|
|
99
|
+
level?: EquipmentLevel;
|
|
100
|
+
parentId?: string;
|
|
101
|
+
}[];
|
|
102
|
+
equipment?: readonly {
|
|
103
|
+
id: string;
|
|
104
|
+
homeLocation?: string;
|
|
105
|
+
}[];
|
|
106
|
+
}): Hierarchy;
|
|
107
|
+
/**
|
|
108
|
+
* 자원 속성 — **ISA-95 가 세 자원에 똑같이 정의한 한 모양.**
|
|
109
|
+
*
|
|
110
|
+
* 1차 출처(B2MML v0701): `EquipmentPropertyType`(B2MML-Equipment.xsd) ·
|
|
111
|
+
* `PersonPropertyType`(B2MML-Personnel.xsd) · `PhysicalAssetPropertyType`(B2MML-PhysicalAsset.xsd).
|
|
112
|
+
* 세 타입의 요소가 동일하다 — `ID` · `Description?` · `Value(ValueType)` · `<X>PropertyChild`(재귀) ·
|
|
113
|
+
* `<X>ClassPropertyID`. 그래서 우리도 **자원마다 다른 모양을 만들지 않는다**(자원별 속성 타입 셋을
|
|
114
|
+
* 두면 그게 방언이 된다).
|
|
115
|
+
*
|
|
116
|
+
* `value`·`dataType`·`uom` 은 표준 `ValueType` 의 `ValueString`·`DataType`·`UnitOfMeasure` 다.
|
|
117
|
+
* 값을 **문자열로 싣는 것도 표준 그대로**다 — 단위와 데이터형이 값 옆에 붙어 있어야 소비처가
|
|
118
|
+
* "3" 이 초속 3m 인지 시속 3km 인지 알 수 있다(단위 없는 숫자는 거절한다는 규율의 근거).
|
|
119
|
+
*
|
|
120
|
+
* 왜 이제 계약에 넣는가: 지금까지 설비 속성(`speed`)이 **계약 밖으로 흘러** 마스터에서 호스트
|
|
121
|
+
* 추정기까지 `any` 로 전달됐다. 자원에 붙는 사실이 계약에 자리가 없으면 소비처마다 다르게 읽는다.
|
|
122
|
+
*/
|
|
123
|
+
export interface ResourceProperty {
|
|
124
|
+
/** 속성 식별자 — 표준 `ID`. 어휘는 표준이 정하지 않으므로 우리가 한 곳에서 정한다(`EQUIPMENT_PROPERTY`). */
|
|
125
|
+
id: string;
|
|
126
|
+
description?: string;
|
|
127
|
+
/** 표준 `Value.ValueString`. 값의 표기는 문자열이고, 뜻은 `dataType`·`uom` 이 정한다. */
|
|
128
|
+
value?: string;
|
|
129
|
+
/** 표준 `Value.DataType` — 예: `xs:double`. 미지정이면 소비처가 형을 짐작하지 않는다. */
|
|
130
|
+
dataType?: string;
|
|
131
|
+
/** 표준 `Value.UnitOfMeasure` — UN/CEFACT 공통코드(예: `MTS`·`KMH`). **단위 없는 물리량은 쓰지 않는다.** */
|
|
132
|
+
uom?: string;
|
|
133
|
+
/** 하위 속성 — 표준 `<X>PropertyChild`(재귀). 구조화된 속성(예: 정격/실측 묶음)을 잃지 않기 위해. */
|
|
134
|
+
children?: ResourceProperty[];
|
|
135
|
+
/** 이 개체 속성이 구체화하는 **등급 속성** — 표준 `<X>ClassPropertyID`. */
|
|
136
|
+
classPropertyId?: string;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* 자격·적격을 **검증한 시험 명세**들 — 표준 `TestSpecificationID`(`maxOccurs="unbounded"`).
|
|
140
|
+
*
|
|
141
|
+
* 1차 출처에서 **자원 타입 9개 중 9개**에 있다(`PersonType`·`PersonnelClassType`·`EquipmentType`·
|
|
142
|
+
* `EquipmentClassType`·`PhysicalAssetType`·`PhysicalAssetClassType`·`MaterialLotType`·
|
|
143
|
+
* `MaterialDefinitionType`·`MaterialClassType`). **`OperationalLocationType` 에만 없다** — 자리는
|
|
144
|
+
* 시험 대상이 아니기 때문이다. 그 비대칭이 표준의 판단이므로 우리도 자리에 넣지 않는다.
|
|
145
|
+
*
|
|
146
|
+
* 이 필드는 **참조만** 한다. 시험 명세 자체(`B2MML-OperationsTest.xsd`)와 시험 결과는 아직 모델에 없다
|
|
147
|
+
* (커버리지 ⬜). 그래서 이 값으로 **검증했다고 주장하지 않는다** — 상류가 "이 자격은 이 시험으로
|
|
148
|
+
* 검증됐다" 고 말해 줄 때 그 사실이 **들어올 자리**를 여는 것이 이 필드의 일이다.
|
|
149
|
+
* 자리가 없으면 사실이 들어오지 못한다.
|
|
150
|
+
*/
|
|
151
|
+
export type TestSpecificationRefs = string[];
|
|
152
|
+
/**
|
|
153
|
+
* 유효 기간 — **ISA-95 `EffectiveStartDate` / `EffectiveEndDate`.**
|
|
154
|
+
*
|
|
155
|
+
* 1차 출처(B2MML v0701): `EquipmentType` · `PersonType` · `PhysicalAssetType` **세 개체 타입 모두**에
|
|
156
|
+
* 있고, 등급 타입 세 개에도 있다. 즉 표준은 이것을 **개체와 등급 양쪽의 공통 축**으로 두었다.
|
|
157
|
+
*
|
|
158
|
+
* **없을 때 무엇이 틀렸나**: 등급에만 있어서(§ResourceClassDef) **폐기한 설비가 영구히 살아 있었다.**
|
|
159
|
+
* 3월에 폐차한 지게차가 7월에도 배정 대상이고, 가용 대수 분모에 들어가 가동률을 낮추고, 화면에 계속
|
|
160
|
+
* 떴다. 도입 예정 설비도 마찬가지로 오늘부터 있는 것처럼 보였다.
|
|
161
|
+
*/
|
|
162
|
+
export interface EffectivePeriod {
|
|
163
|
+
/** 이 시각부터 유효 — 표준 `EffectiveStartDate`. 없으면 "언제부터인지 따지지 않는다". */
|
|
164
|
+
effectiveStart?: ISOTime;
|
|
165
|
+
/** 이 시각까지 유효 — 표준 `EffectiveEndDate`. 없으면 "끝이 정해지지 않았다". */
|
|
166
|
+
effectiveEnd?: ISOTime;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* 유효 기간 밖인 **이유** — 없으면(`undefined`) 유효하다.
|
|
170
|
+
*
|
|
171
|
+
* **왜 참/거짓이 아닌가**: "아직 없다" 와 "이제 없다" 는 사용자가 알고 싶은 **다른 사실**이다. 도입
|
|
172
|
+
* 예정 설비와 폐기한 설비가 화면에서 같은 회색으로 보이면 "왜 안 움직이나" 에 답할 수 없다 —
|
|
173
|
+
* 고장(`down`)·계획정지(`held`)·교대 밖(`offShift`)을 굳이 나눠 둔 것과 같은 이유다.
|
|
174
|
+
*/
|
|
175
|
+
export type Effectivity = 'not-yet' | 'expired';
|
|
176
|
+
/**
|
|
177
|
+
* 이 시각에 유효 기간 밖인가 — **한 규칙**으로 개체·등급·설비↔자산 매핑을 모두 판정한다.
|
|
178
|
+
*
|
|
179
|
+
* **표준은 날짜만 정하고 "밖이면 어떻게 되는가" 는 정하지 않는다.** 그 판단은 우리 것이므로 여기 밝힌다:
|
|
180
|
+
* 기간 밖이면 **그 자원은 그 시각의 모델에 참여하지 않는다**(배정되지 않고, 가용 분모에 들지 않는다).
|
|
181
|
+
* 지우지는 않는다 — 이유를 달아 남긴다. 조용히 사라지면 "왜 없어졌나" 를 아무도 답할 수 없다.
|
|
182
|
+
*
|
|
183
|
+
* `at` 를 주지 않으면 **판단하지 않는다**(`undefined`). 모르는 시각으로 폐기를 단정하면, 시각을 안 넘긴
|
|
184
|
+
* 소비처 전부가 자원을 잃는다. 파싱 불가한 시각도 같다 — 짐작해 고치지 않는다.
|
|
185
|
+
*/
|
|
186
|
+
export declare function effectivityAt(p: EffectivePeriod | undefined, at?: ISOTime): Effectivity | undefined;
|
|
187
|
+
/**
|
|
188
|
+
* 자원 **등급 정의** — ISA-95 가 세 자원에 똑같이 정의한 한 모양.
|
|
189
|
+
*
|
|
190
|
+
* 1차 출처(B2MML v0701): `PersonnelClassType`(B2MML-Personnel.xsd) · `EquipmentClassType`(B2MML-Equipment.xsd)
|
|
191
|
+
* · `PhysicalAssetClassType`(B2MML-PhysicalAsset.xsd). 세 타입이 공통으로 갖는 것 —
|
|
192
|
+
* `ID` · `Description?` · `EffectiveStartDate?` · `EffectiveEndDate?` · **`<X>ClassBaseID`(복수)** ·
|
|
193
|
+
* `<X>ClassProperty`(복수) · `TestSpecificationID`(복수).
|
|
194
|
+
*
|
|
195
|
+
* **정의와 참조는 이름이 다르다.** 표준은 정의를 `<X>Class` 로, 참조를 `<X>ClassID` 로 부른다. 그래서
|
|
196
|
+
* 우리도 정의 목록은 `personnelClasses`, 개체의 소속은 `personnelClassIds` 다 — 한 낱말로 두면
|
|
197
|
+
* "이 필드가 정의인가 참조인가" 를 매번 물어야 한다.
|
|
198
|
+
*
|
|
199
|
+
* **상속이 왜 필요한가**: 요구가 "생산직 2명" 인데 현장에 있는 사람이 "용접 자격자" 라면, 상속이 없으면
|
|
200
|
+
* 그 사람은 요구를 만족하지 못한다. 표준이 `ClassBaseID` 를 복수로 둔 이유가 이것이고(다중 상속),
|
|
201
|
+
* 우리는 소속을 **상속을 타고 닫아** 판정한다(`classClosure`).
|
|
202
|
+
*/
|
|
203
|
+
export interface ResourceClassDef extends EffectivePeriod {
|
|
204
|
+
id: string;
|
|
205
|
+
description?: string;
|
|
206
|
+
/** 상위 등급들 — 표준 `<X>ClassBaseID`(복수). 순환은 `classClosure` 가 끊는다. */
|
|
207
|
+
baseIds?: string[];
|
|
208
|
+
/** 등급 속성 — 표준 `<X>ClassProperty`. 개체 속성이 `classPropertyId` 로 이것을 구체화한다. */
|
|
209
|
+
properties?: ResourceProperty[];
|
|
210
|
+
/** 이 등급의 적격을 정하는 시험 명세들 — 표준 `TestSpecificationID`. */
|
|
211
|
+
testSpecificationIds?: TestSpecificationRefs;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* 등급 소속을 **상속을 타고 닫는다** — "이 개체가 이 등급으로 통하는가".
|
|
215
|
+
*
|
|
216
|
+
* 순환은 방문 집합으로 끊는다(잘못된 마스터가 무한 루프를 만들지 않게). 등급 정의가 없으면 소속
|
|
217
|
+
* 그대로만 본다 — 정의를 요구하지 않는다(정의를 싣지 않은 트윈이 그대로 돌아야 한다).
|
|
218
|
+
*
|
|
219
|
+
* `at` 를 주면 **유효 기간 밖의 등급은 제외**한다. 안 주면 기간을 보지 않는다(모르면 판단하지 않는다).
|
|
220
|
+
*/
|
|
221
|
+
export declare function classClosure(directIds: readonly string[] | undefined, defs: readonly ResourceClassDef[] | undefined, at?: ISOTime): Set<string>;
|
|
222
|
+
/**
|
|
223
|
+
* 우선순위 — **ISA-95 `Priority`**(`JobOrderType`·`OperationsRequestType`, 타입은 `PriorityType` =
|
|
224
|
+
* `NumericType` 제한). 즉 표준은 **숫자라는 것만 정하고 방향은 정하지 않는다.**
|
|
225
|
+
*
|
|
226
|
+
* **그래서 방향은 우리가 정한다: 작은 값이 급하다(1 = 가장 급함).** 흔한 관행이고, 무엇보다
|
|
227
|
+
* 한쪽으로 못 박아 두지 않으면 소비처마다 반대로 읽는다. 우리가 정한 규약이라는 사실을 여기 밝힌다.
|
|
228
|
+
*
|
|
229
|
+
* 미지정은 **0 이 아니라 "우선순위 없음"** 이다 — 선언한 것들 뒤에 선다(0 으로 채우면 미지정이
|
|
230
|
+
* 가장 급한 것이 된다).
|
|
231
|
+
*/
|
|
232
|
+
export declare const PRIORITY_UNSET: number;
|
|
233
|
+
/** 정렬 키 — 미지정을 맨 뒤로 보낸다. 같은 우선순위는 **입력 순서**를 지킨다(결정성). */
|
|
234
|
+
export declare function priorityRank(p?: number): number;
|
|
235
|
+
/**
|
|
236
|
+
* 납기 대비 상태 — **파생**이다(저장하지 않는다). `locationStatusOf` 와 같은 규율.
|
|
237
|
+
*
|
|
238
|
+
* 예정 창(`endTime`)이 없으면 `undefined` — **"늦지 않았다" 가 아니라 "판단할 수 없다"** 다.
|
|
239
|
+
* 납기가 없는데 정시라고 말하면 그건 없는 사실을 만드는 것이다.
|
|
240
|
+
*/
|
|
241
|
+
export declare function dueStatusOf(x: {
|
|
242
|
+
endTime?: ISOTime;
|
|
243
|
+
}, nowIso?: ISOTime): 'on-time' | 'late' | undefined;
|
|
244
|
+
/**
|
|
245
|
+
* 자재 수량 하나 — **ISA-95 `MaterialLot.Quantity`(`maxOccurs="unbounded"`)** 의 한 항목.
|
|
246
|
+
* 타입은 `QuantityValueType` = `QuantityString` + `DataType?` + `UnitOfMeasure?` + `Key?`.
|
|
247
|
+
*
|
|
248
|
+
* **왜 복수인가**: 같은 로트를 여러 단위로 함께 쓴다 — *100 EA · 250 KG · 5 CS*. 창고는 개수로 세고,
|
|
249
|
+
* 운송은 무게로 싣고, 주문은 케이스로 온다. 하나만 담을 수 있으면 나머지는 **버려진다.**
|
|
250
|
+
*
|
|
251
|
+
* **단위를 환산하지 않는다.** 100 EA 가 250 KG 인지는 품목마다 다른 계수이고 우리에게 없다.
|
|
252
|
+
* `quantityIn` 은 **선언된 단위만** 답하고, 없으면 `undefined` 다(계산해서 만들지 않는다).
|
|
253
|
+
*
|
|
254
|
+
* 값은 **숫자**로 든다 — 우리 인제스트 정본이 EPCIS 이고 `QuantityElement.quantity` 가 숫자다.
|
|
255
|
+
* ISA-95 는 문자열(`QuantityString`)로 두지만, 두 표준 사이에서는 EPCIS 쪽을 따른다(§정본).
|
|
256
|
+
*/
|
|
257
|
+
export interface MaterialQuantity {
|
|
258
|
+
value: number;
|
|
259
|
+
/** UN/CEFACT 권고 20 코드(`EA`·`KGM`·`CS`…). 없으면 개수로 읽는다(EPCIS 규약과 같다). */
|
|
260
|
+
uom?: string;
|
|
261
|
+
/** 표준 `DataType` — 미지정이면 형을 짐작하지 않는다. */
|
|
262
|
+
dataType?: string;
|
|
263
|
+
/** 표준 `Key` — 같은 단위가 여러 번 올 때 구별하는 이름(예: 정미/총). */
|
|
264
|
+
key?: string;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* 선언된 단위로 수량을 읽는다 — **환산하지 않는다.**
|
|
268
|
+
*
|
|
269
|
+
* 없으면 `undefined`: "0" 이 아니고 "계산한 값" 도 아니다. 환산 계수를 모르는데 값을 만들면
|
|
270
|
+
* 그 뒤 모든 계산이 거짓 위에 선다.
|
|
271
|
+
*/
|
|
272
|
+
/**
|
|
273
|
+
* 로트의 부분 식별자를 **한 규칙으로** 만든다 — 표준 `MaterialSubLot.ID`.
|
|
274
|
+
*
|
|
275
|
+
* 시뮬(생산)과 미러(관측)가 각자 만들면 같은 부분이 다른 이름을 갖고, 두 구동이 갈라진다
|
|
276
|
+
* (적합성 하네스가 실제로 잡았다). 부분을 가르는 것은 **자리**다.
|
|
277
|
+
*/
|
|
278
|
+
/**
|
|
279
|
+
* 물품을 구별하는 키 — 직렬 물품은 `epc`, 로트의 부분은 `subLotId`(표준 `MaterialSubLot.ID`).
|
|
280
|
+
*
|
|
281
|
+
* **한 곳에서 정한다.** 소비처마다 `epc` 로 키를 잡으면 같은 로트의 두 부분이 하나로 접히고,
|
|
282
|
+
* 그 순간 재고가 조용히 줄어든다(실제로 그랬다 — §ItemState.subLotId).
|
|
283
|
+
*/
|
|
284
|
+
export declare function subLotIdOf(classUri: string, location: string): string;
|
|
285
|
+
export declare function itemKeyOf(item: {
|
|
286
|
+
epc: string;
|
|
287
|
+
subLotId?: string;
|
|
288
|
+
}): string;
|
|
289
|
+
export declare function quantityIn(item: {
|
|
290
|
+
qty?: number;
|
|
291
|
+
uom?: string;
|
|
292
|
+
quantities?: MaterialQuantity[];
|
|
293
|
+
gtin?: string;
|
|
294
|
+
definitionId?: string;
|
|
295
|
+
}, uom?: string, definitions?: MaterialDefinitionIndex): number | undefined;
|
|
296
|
+
/**
|
|
297
|
+
* 품목 정의 — **ISA-95 `MaterialDefinition`.**
|
|
298
|
+
*
|
|
299
|
+
* 1차 출처(B2MML v0701) `MaterialDefinitionType`: `ID` · `Version` · `Description*` · `PublishedDate` ·
|
|
300
|
+
* `EffectiveStartDate`/`EffectiveEndDate` · `HierarchyScope` · `SpatialDefinition` ·
|
|
301
|
+
* **`MaterialDefinitionProperty*`** · **`MaterialClassID*`(복수)** · `MaterialLotSourceID*` ·
|
|
302
|
+
* `TestSpecificationID*` · `AssemblyDefinition*`.
|
|
303
|
+
*
|
|
304
|
+
* ── 없을 때 무엇이 안 됐나 ────────────────────────────────────────────────
|
|
305
|
+
* 로트만 관측으로 들어오고 **품목이 무엇인지는 아무 데도 없었다.** 그래서 같은 로트를 100 EA 로
|
|
306
|
+
* 받아도 "몇 kg 인가" 에 답할 수 없었고(§quantityIn 이 환산을 거부하는 근거), 화면은 GTIN 원문을
|
|
307
|
+
* 그대로 보여 줄 수밖에 없었다.
|
|
308
|
+
*
|
|
309
|
+
* ── 표준에는 환산 요소가 없다 ─────────────────────────────────────────────
|
|
310
|
+
* 전수 대조 결과 `MaterialDefinitionType` 에 단위 환산을 담을 **전용 요소가 없다.** 표준이 주는 것은
|
|
311
|
+
* **속성 주머니**(`MaterialDefinitionProperty`)뿐이다 — 설비 속도(`EQUIPMENT_PROPERTY.speed`)와 같은
|
|
312
|
+
* 상황이다. 그래서 자리는 표준 것을 쓰고 **이름은 우리가 정하고 밝힌다**(§MATERIAL_PROPERTY).
|
|
313
|
+
*/
|
|
314
|
+
export interface MaterialDefinition extends EffectivePeriod {
|
|
315
|
+
/** 표준 `ID` — 우리는 **GTIN**(`urn:epc:idpat:sgtin:…`)을 쓴다. 정체성의 정본이 EPCIS 이기 때문이다. */
|
|
316
|
+
id: string;
|
|
317
|
+
description?: string;
|
|
318
|
+
/** 속한 등급들 — 표준 `MaterialClassID`(**복수**). 인원·자산과 같은 이유로 복수다. */
|
|
319
|
+
materialClassIds?: string[];
|
|
320
|
+
/** 품목 속성 — 표준 `MaterialDefinitionProperty`. 환산 계수가 여기 들어간다(§MATERIAL_PROPERTY). */
|
|
321
|
+
properties?: ResourceProperty[];
|
|
322
|
+
/** 적격을 검증한 시험 명세들 — 표준 `MaterialDefinition.TestSpecificationID`. */
|
|
323
|
+
testSpecificationIds?: TestSpecificationRefs;
|
|
324
|
+
}
|
|
325
|
+
/** 품목 정의 색인 — id(GTIN) → 정의. 소비처가 매번 배열을 훑지 않게. */
|
|
326
|
+
export type MaterialDefinitionIndex = ReadonlyMap<string, MaterialDefinition>;
|
|
327
|
+
/**
|
|
328
|
+
* **우리가 정한 품목 속성 이름** — 표준은 자리만 정하고 이름을 정하지 않는다.
|
|
329
|
+
*
|
|
330
|
+
* `perBaseUnit`: 값은 **기준 단위 하나당 그 단위의 양**이고, 단위는 속성의 `uom` 이 말한다.
|
|
331
|
+
* 예) 한 개(EA)가 2.5 kg 이면 `{ id: 'perBaseUnit', value: 2.5, uom: 'KGM' }`.
|
|
332
|
+
*
|
|
333
|
+
* **왜 이 모양인가**: 임의의 단위쌍 환산표(CS↔KGM↔EA…)는 품목마다 다르고 연쇄가 필요해 금세
|
|
334
|
+
* 커진다. 현장에서 실제로 필요한 것은 **"세는 단위에서 다른 단위로"** 이므로, 기준 단위를 축으로
|
|
335
|
+
* 두면 선언 한 줄로 끝난다. 기준 단위가 아닌 수량에서 출발하는 환산은 **하지 않는다**(§convertQuantity).
|
|
336
|
+
*/
|
|
337
|
+
export declare const MATERIAL_PROPERTY: {
|
|
338
|
+
/** 기준 단위 하나당 이 단위의 양. `uom` 이 대상 단위. */
|
|
339
|
+
readonly perBaseUnit: "perBaseUnit";
|
|
340
|
+
};
|
|
341
|
+
/**
|
|
342
|
+
* 이 품목에서 `uom` 으로 가는 계수 — **선언된 것만.** 없으면 `undefined`(추정하지 않는다).
|
|
343
|
+
*/
|
|
344
|
+
export declare function conversionFactorOf(def: MaterialDefinition | undefined, uom?: string): number | undefined;
|
|
345
|
+
/**
|
|
346
|
+
* 근무·비근무 구간 하나 — **ISA-95 `WorkCalendarEntry`.**
|
|
347
|
+
*
|
|
348
|
+
* 1차 출처(B2MML v0701) `WorkCalendarEntryType`: `ID` · `Description*` · `StartDateTime` ·
|
|
349
|
+
* `FinishDateTime` · `EntryType` · `WorkCalendarEntryChild*` · `WorkCalendarEntryProperty*`.
|
|
350
|
+
* 반복 규칙은 `WorkCalendarDefinitionEntryType.RecurrenceTime`/`DurationRule` 에 있다.
|
|
351
|
+
*
|
|
352
|
+
* **프레임워크와 같은 모양으로 맞췄다** — `@things-factory/work-shift` 의 `WorkShift` 는
|
|
353
|
+
* `fromDate`(전일 −1 · 당일 0 · 익일 +1) · `fromTime` · `toDate` · `toTime` 로 교대를 든다.
|
|
354
|
+
* 그것이 자정을 넘는 교대를 정확히 표현하는 모양이고, 표준의 Start/Finish 와도 맞는다. 그래서 우리도
|
|
355
|
+
* 같은 축을 쓴다 — 나중에 그 엔티티를 정본으로 결선할 때 **직선 매핑**이 되게(재발명 금지).
|
|
356
|
+
*
|
|
357
|
+
* 여기 담는 것은 **하루 안의 되풀이 구간**이다(달력 날짜가 아니다). 특정 날짜의 휴일·정비는 표준
|
|
358
|
+
* `WorkCalendarEntry` 의 `StartDateTime`/`FinishDateTime` 이 담는 영역이고 아직 없다(⬜).
|
|
359
|
+
*
|
|
360
|
+
* ── 시각의 기준을 반드시 밝혀야 한다 ──────────────────────────────────────
|
|
361
|
+
* 표준은 `StartDateTime`/`FinishDateTime`(**절대 시각**)을 쓴다. 우리가 시각대(`HH:MM`)로 두는 것은
|
|
362
|
+
* 되풀이를 간단히 적기 위한 선택이고, 그러면 **어느 기준의 06:00 인가**가 반드시 필요하다 —
|
|
363
|
+
* Rosarito(UTC−7)의 06:00 을 UTC 로 읽으면 7시간이 틀린다.
|
|
364
|
+
*
|
|
365
|
+
* 그래서 `BoardDef.utcOffsetMinutes` 가 그 기준이다. **선언하지 않으면 UTC 로 읽고, 그 사실을 여기
|
|
366
|
+
* 밝힌다**(조용히 가정하지 않기 위해). 프레임워크에는 이미 테넌트 시간대(`Domain.timezone`)와
|
|
367
|
+
* 교대→절대구간 변환(`@things-factory/work-shift` `work-shift-range`, moment-timezone)이 있으므로,
|
|
368
|
+
* **기준을 푸는 일은 호스트의 몫**이다(커널은 zero-dep — 시간대 데이터를 들 수 없다).
|
|
369
|
+
*
|
|
370
|
+
* 한계도 적는다: 고정 오프셋은 **일광절약시간을 따라가지 못한다.** 긴 지평선에서 정확히 하려면
|
|
371
|
+
* 호스트가 표준대로 **절대 구간**을 계산해 넣어야 한다(⬜ — 그때 이 필드는 필요 없어진다).
|
|
372
|
+
*/
|
|
373
|
+
export interface WorkCalendarEntry {
|
|
374
|
+
/**
|
|
375
|
+
* 표준 `WorkCalendarEntryType.ID` — **교대의 이름**(`A`·`B`·`C`, `night`…).
|
|
376
|
+
*
|
|
377
|
+
* 3교대처럼 **하루를 끊김 없이 덮는** 현장에서는 이 이름이 이 선언의 거의 전부다: 설비 가용은
|
|
378
|
+
* 24시간 그대로이므로 "쉬는 시간" 은 생기지 않고, 대신 **"이 일이 어느 교대에 일어났나"** 를
|
|
379
|
+
* 말할 수 있게 된다. 그 축이 없으면 교대별 성과를 물을 수 없다(`activeShiftOf`).
|
|
380
|
+
*/
|
|
381
|
+
id?: string;
|
|
382
|
+
/**
|
|
383
|
+
* **한 번뿐인 구간** — 표준 `WorkCalendarEntry.StartDateTime` / `FinishDateTime`(절대 시각).
|
|
384
|
+
*
|
|
385
|
+
* ── 표준은 둘로 나눈다 ───────────────────────────────────────────────────
|
|
386
|
+
* `WorkCalendarEntryType` 은 **절대 구간**이고, 되풀이 규칙은 `WorkCalendarDefinitionEntryType`
|
|
387
|
+
* (`RecurrenceTime`/`DurationRule`)에 있으며 항목이 `WorkCalendarDefintionEntryID` 로 그것을 가리킨다.
|
|
388
|
+
* 우리 `fromTime`/`toTime` 은 **되풀이 쪽**에 해당하는 우리식 단순형이고, **절대 구간 자체가 없었다.**
|
|
389
|
+
*
|
|
390
|
+
* 그래서 **휴일·연휴·특정일 정비창을 표현할 수 없었다** — 하루 안에서 반복되는 것만 말할 수 있었다.
|
|
391
|
+
* 8월 15일 하루를 쉰다는 사실을 `HH:MM` 으로는 적을 방법이 없다.
|
|
392
|
+
*
|
|
393
|
+
* 절대 구간은 **그 자체로 완결**이라 시각 기준(`utcOffsetMinutes`)이 필요 없다 — ISO 시각에 이미
|
|
394
|
+
* 들어 있다. 일광절약시간 문제도 여기서는 생기지 않는다(§4-7 이 남긴 한계가 이 항목에는 없다).
|
|
395
|
+
*/
|
|
396
|
+
startDateTime?: ISOTime;
|
|
397
|
+
finishDateTime?: ISOTime;
|
|
398
|
+
/**
|
|
399
|
+
* **어느 요일에만** — 0 일요일 … 6 토요일. 없으면 매일이다(기존 거동).
|
|
400
|
+
*
|
|
401
|
+
* ── 표준에 되풀이 표기가 없다 (1차 출처 확인) ────────────────────────────
|
|
402
|
+
* `WorkCalendarDefinitionEntryType.RecurrenceTime`·`DurationRule` 은 **`CodeType`**, 즉 **불투명한
|
|
403
|
+
* 코드**다 — 표준은 자리만 두고 문법을 정하지 않는다. 설비 속도(`EQUIPMENT_PROPERTY.speed`)·
|
|
404
|
+
* 단위 환산(`MATERIAL_PROPERTY.perBaseUnit`)과 같은 상황이라, **우리가 정하고 밝힌다.**
|
|
405
|
+
*
|
|
406
|
+
* ── 없을 때 무엇을 못 했나 ───────────────────────────────────────────────
|
|
407
|
+
* `HH:MM` 되풀이는 **하루 안**만 말한다. 그래서 **주말에 안 도는 공장을 표현할 수 없었다** —
|
|
408
|
+
* 토·일을 쉬는 현장이 24시간 x 7일 도는 것으로 예측됐고, 그만큼 처리량이 부풀었다.
|
|
409
|
+
*
|
|
410
|
+
* 요일은 **선언된 시각 기준**으로 읽는다(Rosarito 의 일요일과 UTC 의 일요일은 다르다).
|
|
411
|
+
*
|
|
412
|
+
* ── 자정을 넘는 교대는 **시작한 날**로 센다 ─────────────────────────────
|
|
413
|
+
* 야간 교대(22:00→06:00)에 `[1..5]`(월~금)를 주면 뜻은 **"월~금에 시작한다"** 이다. 그래야
|
|
414
|
+
* **금요일 밤 교대가 토요일 새벽까지** 이어지고, **일요일 밤에 없는 교대가 월요일 새벽에 생기지
|
|
415
|
+
* 않는다.** 순간의 요일로 세면 그 둘이 정확히 반대로 틀린다.
|
|
416
|
+
*/
|
|
417
|
+
daysOfWeek?: number[];
|
|
418
|
+
/** 시작 날짜 오프셋 — `-1` 전일 · `0` 당일 · `+1` 익일(프레임워크 `WorkShiftDateType` 와 같은 축). */
|
|
419
|
+
fromDayOffset?: -1 | 0 | 1;
|
|
420
|
+
/** 시작 시각 `HH:MM`(24시간). 분 단위까지 — 시(hour)만으로는 07:30 교대를 표현할 수 없다.
|
|
421
|
+
* **절대 구간(`startDateTime`)을 쓰는 항목에는 없다.** */
|
|
422
|
+
fromTime?: string;
|
|
423
|
+
toDayOffset?: -1 | 0 | 1;
|
|
424
|
+
/** 종료 시각 `HH:MM`. 시작보다 이르면 자정을 넘는 것으로 읽는다. */
|
|
425
|
+
toTime?: string;
|
|
426
|
+
/**
|
|
427
|
+
* 표준 `EntryType` — 이 구간이 **근무**인가 **비근무**(휴일·정비·휴게)인가.
|
|
428
|
+
* 미지정은 `working`(선언한 구간은 일하는 시간이라는 흔한 뜻).
|
|
429
|
+
*/
|
|
430
|
+
entryType?: 'working' | 'non-working';
|
|
431
|
+
}
|
|
432
|
+
/** 선언된 기준의 요일(0 일요일 … 6 토요일). */
|
|
433
|
+
export declare function weekdayAt(ms: number, utcOffsetMinutes?: number): number;
|
|
434
|
+
export declare function inWorkCalendar(entries: readonly WorkCalendarEntry[] | undefined, minuteOfDay: number): boolean;
|
|
435
|
+
/**
|
|
436
|
+
* 이 **시각**이 근무 시간인가 — 되풀이(`HH:MM`)와 **한 번뿐인 구간**(휴일·정비창)을 함께 본다.
|
|
437
|
+
*
|
|
438
|
+
* 절대 구간은 시각 기준이 필요 없다(ISO 시각에 이미 들어 있다). 되풀이는 **선언된 기준**으로 읽는다.
|
|
439
|
+
* 겹치면 규칙은 하나다 — **비근무가 근무를 이긴다**(휴일이 교대 위에 얹힌다).
|
|
440
|
+
*/
|
|
441
|
+
export declare function inWorkCalendarAt(entries: readonly WorkCalendarEntry[] | undefined, atMs: number, utcOffsetMinutes?: number): boolean;
|
|
442
|
+
/**
|
|
443
|
+
* 지금 어느 교대인가 — **근무 구간의 이름**(표준 `WorkCalendarEntryType.ID`).
|
|
444
|
+
*
|
|
445
|
+
* 비근무가 이기는 규칙은 여기서도 같다: 휴게·정비 중이면 **어느 교대도 아니다**(`undefined`).
|
|
446
|
+
* 이름 없는 구간은 이름을 지어내지 않는다 — 교대를 나눠 놓지 않은 현장에서 `'1'` 같은 값을
|
|
447
|
+
* 만들어 붙이면, 그 뒤 모든 교대별 집계가 없는 구분 위에 선다.
|
|
448
|
+
*
|
|
449
|
+
* 겹치는 근무 구간이 여럿이면 **먼저 선언된 것**을 답한다(선언 순서가 현장의 우선순위다).
|
|
450
|
+
*/
|
|
451
|
+
export declare function activeShiftOf(entries: readonly WorkCalendarEntry[] | undefined, minuteOfDay: number): string | undefined;
|
|
452
|
+
/**
|
|
453
|
+
* 이 **시각**에 어느 교대인가 — 휴일까지 반영한다.
|
|
454
|
+
*
|
|
455
|
+
* 휴일에 일어난 일은 **어느 교대에도 속하지 않는다**(그날 교대는 서지 않았다). 되풀이만 보는
|
|
456
|
+
* `activeShiftOf` 로는 그것을 알 수 없어, 휴일에 찍힌 기록이 평소 교대로 집계됐다.
|
|
457
|
+
*/
|
|
458
|
+
export declare function activeShiftAt(entries: readonly WorkCalendarEntry[] | undefined, atMs: number, utcOffsetMinutes?: number): string | undefined;
|
|
459
|
+
/**
|
|
460
|
+
* 절대 시각(ms) → **선언된 기준의** 하루 중 분(0..1439).
|
|
461
|
+
*
|
|
462
|
+
* `HH:MM` 만으로는 "어느 기준의 06시" 인지 알 수 없다. 예전에 이것을 UTC 로 읽어 Rosarito(UTC−7)의
|
|
463
|
+
* 06시 교대가 **7시간 틀렸다.** 선언이 없으면 UTC 이고, 그 기본값을 숨기지 않고 밝힌다.
|
|
464
|
+
*/
|
|
465
|
+
export declare function minuteOfDayAt(ms: number, utcOffsetMinutes?: number): number;
|
|
466
|
+
/**
|
|
467
|
+
* 이 시각에 **근무 시간 밖인가** — 캘린더가 있으면 그것으로, 없으면 옛 `window` 로 판정한다.
|
|
468
|
+
*
|
|
469
|
+
* **두 구동이 이 함수를 함께 쓴다.** 예전에는 이 판정이 시뮬 안에만 있고 미러에는 **아예 없었다** —
|
|
470
|
+
* 설비 델타에는 `offShift` 자리조차 없어서, 미러의 설비는 점심 휴게 중에도 "대기" 로 보였다.
|
|
471
|
+
* 사람 델타에는 자리가 있었지만 그것은 **전이 순간의 값**이라, 유휴로 교대 경계를 넘긴 사람은
|
|
472
|
+
* 옛 판정에 머물렀다. 시각으로만 바뀌는 사실은 실어 보낼 수 없고 **각자 계산해야** 한다.
|
|
473
|
+
*/
|
|
474
|
+
/**
|
|
475
|
+
* **왜 안 하고 있나** — 쉬는 이유를 가른다.
|
|
476
|
+
*
|
|
477
|
+
* `offShift` 만으로는 화면이 "교대 밖" 이라고밖에 못 말한다. 그런데 사용자가 알고 싶은 것은
|
|
478
|
+
* **휴일이라 오늘 통째로 서는 것인지**, 잠깐 휴게인지, 그냥 교대 시간이 아닌지다 — 셋은 기다릴
|
|
479
|
+
* 시간도 할 일도 다르다. 신정에 24시간 멈춘 트윈이 그냥 빈 화면으로 보이면 고장으로 읽힌다.
|
|
480
|
+
*
|
|
481
|
+
* `'non-working'` 선언된 비근무 구간(휴일·휴게·정비창)이 덮었다.
|
|
482
|
+
* `'off-hours'` 근무 구간이 하나도 안 덮었다(주말·교대 사이).
|
|
483
|
+
*/
|
|
484
|
+
export type OffCalendarReason = 'non-working' | 'off-hours';
|
|
485
|
+
export declare function offCalendarReasonAt(r: {
|
|
486
|
+
window?: {
|
|
487
|
+
startHour: number;
|
|
488
|
+
endHour: number;
|
|
489
|
+
};
|
|
490
|
+
workCalendar?: WorkCalendarEntry[];
|
|
491
|
+
}, ms: number, utcOffsetMinutes?: number): OffCalendarReason | undefined;
|
|
492
|
+
export declare function offCalendarAt(r: {
|
|
493
|
+
window?: {
|
|
494
|
+
startHour: number;
|
|
495
|
+
endHour: number;
|
|
496
|
+
};
|
|
497
|
+
workCalendar?: WorkCalendarEntry[];
|
|
498
|
+
}, ms: number, utcOffsetMinutes?: number): boolean;
|
|
499
|
+
export interface LocationState {
|
|
26
500
|
id: string;
|
|
27
501
|
type: string;
|
|
28
502
|
/**
|
|
@@ -43,8 +517,32 @@ export interface NodeState {
|
|
|
43
517
|
*/
|
|
44
518
|
parallelism?: number;
|
|
45
519
|
occupancy: number;
|
|
46
|
-
/** 포화도 파생 상태 — `
|
|
520
|
+
/** 포화도 파생 상태 — `locationStatusOf` 가 낸다(두 구동이 같은 함수를 쓴다). 용량 미상이면 없다. */
|
|
47
521
|
status?: string;
|
|
522
|
+
/**
|
|
523
|
+
* **ISA-95 설비 계층 단계** — `ProductionLine`·`StorageZone`·`WorkCenter` 등(`EQUIPMENT_LEVEL`).
|
|
524
|
+
*
|
|
525
|
+
* `type` 과 다른 축이다: `type` 은 현장의 낱말(`paint-booth`·`cut-station`)이고, 이것은 **표준이
|
|
526
|
+
* 정한 역할**이다. 둘을 겹쳐 두는 이유 — 연동 상대와는 표준 축으로 말하고, 화면에는 현장 낱말을
|
|
527
|
+
* 보여줘야 한다. 하나만 두면 한쪽을 잃는다.
|
|
528
|
+
*
|
|
529
|
+
* 미지정이면 그 자리의 표준 역할을 **아직 모른다**는 뜻이다. 짐작해 채우지 않는다.
|
|
530
|
+
*/
|
|
531
|
+
level?: EquipmentLevel;
|
|
532
|
+
/**
|
|
533
|
+
* **상위 자리** — 주목 롤업·구역 매핑용(마스터 계층에서 유래).
|
|
534
|
+
*
|
|
535
|
+
* 가리키는 대상은 **두 가지 중 하나**이고, 둘은 조회로 구별된다(`locations` 에 그 id 가 있는지):
|
|
536
|
+
* - **다른 자리** — 중간 단(라인·셀·존). 계층은 여기서 깊어진다.
|
|
537
|
+
* - **자리가 아닌 것** — 공간의 구역(area). 사슬의 끝이다.
|
|
538
|
+
*
|
|
539
|
+
* 왜 깊이가 필요한가: 최종조립 16개 스테이션이 두 라인에 속하는데 중간 단이 없으면 전부 구역에
|
|
540
|
+
* 평평하게 붙고, **"라인 1의 처리량"을 물을 방법이 없다.** 현장은 라인 단위로 관리되는데 모델에
|
|
541
|
+
* 그 단이 없으면 관리 단위와 모델이 어긋난다.
|
|
542
|
+
*
|
|
543
|
+
* **사슬은 직접 걷지 말 것** — `ancestorsOf`·`rollupOf`·`descendantsOf` 를 쓴다. 한 홉만 보는 코드는
|
|
544
|
+
* 중간 단이 생기는 순간 그 아래 자리를 **집계에서 조용히 빠뜨린다**(정합성이 아니라 침묵이 문제다).
|
|
545
|
+
*/
|
|
48
546
|
parentId?: string;
|
|
49
547
|
/**
|
|
50
548
|
* 이 자리를 **어떻게 알게 됐는가** — `master`(원 시스템 마스터/저작이 말해 준 자리) ·
|
|
@@ -63,6 +561,29 @@ export interface NodeState {
|
|
|
63
561
|
export interface ItemState {
|
|
64
562
|
/** 식별자 — 개체(urn:epc:id:…) 또는 클래스(urn:epc:class:lgtin:… / urn:epc:idpat:…). */
|
|
65
563
|
epc: string;
|
|
564
|
+
/**
|
|
565
|
+
* **로트의 한 부분** — 표준 `MaterialSubLot.ID`.
|
|
566
|
+
*
|
|
567
|
+
* ── 없을 때 무엇이 사라졌나 ──────────────────────────────────────────────
|
|
568
|
+
* 비직렬 로트(LGTIN)의 키가 **로트 식별자 하나**였다. 그래서 같은 로트를 rack-1 에 100개,
|
|
569
|
+
* rack-2 에 60개 관측하면 **뒤에 온 관측이 앞을 덮어 100개가 조용히 사라졌다**(합계 160 → 60).
|
|
570
|
+
* 로트가 여러 자리에 나뉘어 놓이는 것은 창고에서 일상이다.
|
|
571
|
+
*
|
|
572
|
+
* 표준은 이 자리를 `MaterialSubLot` 으로 둔다 — 각 부분이 **자기 `ID`·`StorageLocation`·`Quantity`**
|
|
573
|
+
* 를 갖는다. 그래서 우리도 부분마다 한 줄로 든다: `epc` 는 여전히 **로트의 식별자**(같은 로트의 두
|
|
574
|
+
* 부분은 같은 `epc` 를 갖는다)이고, **개체를 구별하는 키는 이것**이다.
|
|
575
|
+
*
|
|
576
|
+
* 직렬 물품(SGTIN)은 그 자체가 유일하므로 비어 있다 — 소비처는 `subLotId ?? epc` 로 키를 잡는다.
|
|
577
|
+
* **EPC 처럼 생긴 식별자를 지어내지 않는다**(그러면 파서가 깨지고 상류의 것과 구별할 수 없다).
|
|
578
|
+
*/
|
|
579
|
+
subLotId?: string;
|
|
580
|
+
/**
|
|
581
|
+
* 이 로트가 **무슨 품목인가** — 표준 `MaterialLot.MaterialDefinitionID`.
|
|
582
|
+
*
|
|
583
|
+
* 보통은 `gtin` 이 곧 정의 id 라 비어 있다(정체성의 정본이 EPCIS 다). 상류가 GTIN 과 다른 품목 코드를
|
|
584
|
+
* 쓸 때 그 사실이 **들어올 자리**가 이것이다 — 자리가 없으면 사실이 들어오지 못한다.
|
|
585
|
+
*/
|
|
586
|
+
definitionId?: string;
|
|
66
587
|
/**
|
|
67
588
|
* 품목 클래스 식별자 — **URI 원문 그대로**(`urn:epc:idpat:sgtin:…` 또는 `urn:epc:class:lgtin:…`).
|
|
68
589
|
* 오더의 skuMix·할당이 이 값으로 매칭하므로 뜻을 바꾸지 않는다.
|
|
@@ -85,6 +606,13 @@ export interface ItemState {
|
|
|
85
606
|
qty?: number;
|
|
86
607
|
/** 수량 단위(UN/ECE Rec 20 코드: EA·KGM 등). 없으면 개수로 읽는다. */
|
|
87
608
|
uom?: string;
|
|
609
|
+
/**
|
|
610
|
+
* **선언된 모든 수량** — 표준 `MaterialLot.Quantity`(복수). 같은 로트가 100 EA 이면서 250 KG 다.
|
|
611
|
+
*
|
|
612
|
+
* `qty`/`uom` 은 그중 **계산에 쓰는 주 수량**이다(배정·이행 산식이 이 값으로 굳어 있다).
|
|
613
|
+
* `quantities` 는 **받은 것 전부**이고 주 수량도 그 안에 든다 — 읽을 때는 `quantityIn` 을 쓴다.
|
|
614
|
+
*/
|
|
615
|
+
quantities?: MaterialQuantity[];
|
|
88
616
|
expiry?: number;
|
|
89
617
|
/**
|
|
90
618
|
* 개체·로트 마스터데이터 원문 — 표준이 속성 이름을 정의하지 않으므로(상위 문서 소관) **받은 것을
|
|
@@ -96,9 +624,9 @@ export interface ItemState {
|
|
|
96
624
|
* 운영·키네마틱 모션 — State 이원 모델의 "연속" 절반.
|
|
97
625
|
* 백엔드는 이동 시작 시 이 값(from/to/duration)만 방출하고, UI 는 progress 를
|
|
98
626
|
* 로컬 프레임레이트로 보간한다(매 tick 통신 아님). 좌표는 커널 토폴로지 수준이 아니라
|
|
99
|
-
* 보드 바인딩에서
|
|
627
|
+
* 보드 바인딩에서 자리→좌표로 해석. 상세: design/simulation/execution-model.md §4·§5
|
|
100
628
|
*/
|
|
101
|
-
export interface
|
|
629
|
+
export interface EquipmentMotion {
|
|
102
630
|
fromNode: string;
|
|
103
631
|
toNode: string;
|
|
104
632
|
startedAtSimMs: number;
|
|
@@ -124,13 +652,26 @@ export interface OeeMetrics {
|
|
|
124
652
|
goodCount: number;
|
|
125
653
|
scrapCount: number;
|
|
126
654
|
}
|
|
127
|
-
export interface
|
|
655
|
+
export interface EquipmentState extends EffectivePeriod {
|
|
128
656
|
id: string;
|
|
129
657
|
kind: string;
|
|
658
|
+
/** **지금 어디에 있나.** 운반 작업이 끝나면 도착 자리로 옮겨진다(제자리 작업은 안 움직인다). */
|
|
130
659
|
location?: string;
|
|
660
|
+
/**
|
|
661
|
+
* **어디에 속하나** — 붙박인 자리(마스터의 `homeLocationId`). `location` 과 다른 질문이다.
|
|
662
|
+
*
|
|
663
|
+
* 이 필드가 **고정 설비와 이동 설비를 가른다** — 새 타입 플래그 없이:
|
|
664
|
+
* - 도장기·용접로봇은 평생 그 자리에 있다 → `location === homeLocation` 가 항상 성립.
|
|
665
|
+
* - 지게차·호슬러는 돌아다닌다 → 둘이 갈린다. 그래도 소속은 `homeLocation` 하나로 안정적이다.
|
|
666
|
+
*
|
|
667
|
+
* 그래서 "이 라인의 설비 가동률" 은 `homeLocation` 로 묻고(소속), "지금 이 자리에 누가 와 있나" 는
|
|
668
|
+
* `location` 으로 묻는다. 예전에는 소속이 적재 시점에 `location` 초기값으로 소비되고 **버려졌다** —
|
|
669
|
+
* 그래서 이동 설비가 한 번 움직이면 원래 소속을 아무도 알 수 없었다.
|
|
670
|
+
*/
|
|
671
|
+
homeLocation?: string;
|
|
131
672
|
status: string;
|
|
132
673
|
taskId?: string;
|
|
133
|
-
motion?:
|
|
674
|
+
motion?: EquipmentMotion;
|
|
134
675
|
oee?: OeeMetrics;
|
|
135
676
|
held?: boolean;
|
|
136
677
|
/**
|
|
@@ -138,28 +679,92 @@ export interface MoverState {
|
|
|
138
679
|
* 셋을 뭉개면 "왜 안 움직이나" 에 답할 수 없다(고쳐야 하나·풀어야 하나·기다려야 하나).
|
|
139
680
|
*/
|
|
140
681
|
offShift?: boolean;
|
|
141
|
-
/**
|
|
682
|
+
/**
|
|
683
|
+
* 유효 기간 밖이라 이 시각의 모델에 없다 — **네 번째 이유**(§Effectivity).
|
|
684
|
+
* 도입 예정(`not-yet`)과 폐기(`expired`)를 구별한다. 유효하면 값이 없다.
|
|
685
|
+
*/
|
|
686
|
+
effectivity?: Effectivity;
|
|
687
|
+
/**
|
|
688
|
+
* **왜 쉬나** — `offShift` 가 참일 때만 있다(§OffCalendarReason).
|
|
689
|
+
* `non-working` 휴일·휴게·정비창 · `off-hours` 주말·교대 사이. 셋은 기다릴 시간도 할 일도 다르다.
|
|
690
|
+
*/
|
|
691
|
+
offShiftReason?: OffCalendarReason;
|
|
692
|
+
/**
|
|
693
|
+
* 지금 어느 교대인가 — 표준 `WorkCalendarEntry.ID`(§activeShiftOf).
|
|
694
|
+
*
|
|
695
|
+
* **파생값이다** — 델타로 실어 보내지 않는다(교대 경계는 이벤트 없이 시각만으로 넘어간다).
|
|
696
|
+
* 3교대처럼 하루를 끊김 없이 덮는 현장에서 이 값이 **교대별 성과를 물을 수 있는 유일한 축**이다.
|
|
697
|
+
*/
|
|
698
|
+
shift?: string;
|
|
699
|
+
/** 이 자원을 어떻게 알게 됐는가 — LocationState.origin 과 같은 뜻(성장 정책을 한 규칙으로 선언). */
|
|
142
700
|
origin?: 'master' | 'observed';
|
|
701
|
+
/**
|
|
702
|
+
* 자원 속성 — 표준 `EquipmentProperty`. 속도(`EQUIPMENT_PROPERTY.speed`)처럼 **소비처가 읽는 사실**이
|
|
703
|
+
* 여기 실린다. 지금까지 계약 밖으로 흘러 호스트까지 `any` 로 전달됐다(§ResourceProperty).
|
|
704
|
+
*/
|
|
705
|
+
properties?: ResourceProperty[];
|
|
706
|
+
/** 근무 캘린더 — 표준 `WorkCalendarEntry`(복수). 가동시간·정비창을 함께 표현한다. */
|
|
707
|
+
workCalendar?: WorkCalendarEntry[];
|
|
708
|
+
/** 적격을 검증한 시험 명세들 — 표준 `Equipment.TestSpecificationID`(§TestSpecificationRefs). */
|
|
709
|
+
testSpecificationIds?: TestSpecificationRefs;
|
|
143
710
|
}
|
|
144
711
|
/**
|
|
145
712
|
* 사람 — **ISA-95 `Person`.** 설비와 다른 자원 종류다.
|
|
146
713
|
*
|
|
147
|
-
* 왜
|
|
714
|
+
* 왜 설비(설비)로 뭉개지 않는가: 사람은 고장 나지 않고(MTBF), 설비종합효율로 평가하지 않으며,
|
|
148
715
|
* **등급(자격)과 교대로 산다.** 같은 그릇에 담으면 설비의 어휘(고장·수리·OEE)가 사람에게 붙고
|
|
149
716
|
* 사람의 어휘(등급·교대·투입 인원)가 설비에 붙는다 — 둘 다 거짓이 된다.
|
|
150
717
|
*
|
|
151
718
|
* 그리고 **인원은 현장에서 가장 자주 부족한 자원**이다. 모델에 없으면 "사람을 두 명 더 넣으면
|
|
152
719
|
* 어떻게 되나" 를 물을 수 없고, 사람이 만들어 내는 줄이 예측에서 통째로 사라진다.
|
|
153
720
|
*/
|
|
154
|
-
export interface PersonState {
|
|
721
|
+
export interface PersonState extends EffectivePeriod {
|
|
155
722
|
id: string;
|
|
156
|
-
/**
|
|
157
|
-
|
|
723
|
+
/**
|
|
724
|
+
* 소속 등급들 — **ISA-95 `Person.PersonnelClassID`, `maxOccurs="unbounded"`**(B2MML-Personnel.xsd).
|
|
725
|
+
*
|
|
726
|
+
* **복수인 것이 표준이고, 그것이 표준의 자격 표현이다.** 한 사람이 용접 자격과 지게차 자격을 함께
|
|
727
|
+
* 갖는다 — 예전에는 이 필드가 문자열 하나여서 그 사람을 두 작업 중 하나에만 배정할 수 있었다
|
|
728
|
+
* (자격 하나를 고르면 나머지 자격이 사라지는 모델).
|
|
729
|
+
*
|
|
730
|
+
* 배정은 개인 지목이 아니라 **등급으로 요구**되고(`OpPersonnelSpecification` = ClassID + Quantity),
|
|
731
|
+
* 사람이 그 등급 중 하나를 **포함**하면 자격이 성립한다.
|
|
732
|
+
*/
|
|
733
|
+
personnelClassIds?: string[];
|
|
158
734
|
/** 'idle' | 'busy'. 고장(down)이 없다 — 사람은 그렇게 모델링하지 않는다. */
|
|
159
735
|
status: string;
|
|
160
736
|
taskId?: string;
|
|
161
|
-
/** 교대 밖 — 자원(
|
|
737
|
+
/** 교대 밖 — 자원(EquipmentState.offShift)과 같은 뜻. */
|
|
162
738
|
offShift?: boolean;
|
|
739
|
+
/**
|
|
740
|
+
* 유효 기간 밖 — 설비와 같은 뜻(§Effectivity). 사람에게는 **입사 전·퇴사 후**가 이것이다
|
|
741
|
+
* (자격 만료는 등급 쪽 유효 기간이다 — 사람은 남고 자격만 끊긴다).
|
|
742
|
+
*/
|
|
743
|
+
effectivity?: Effectivity;
|
|
744
|
+
/**
|
|
745
|
+
* 지금 어느 교대인가 — 표준 `WorkCalendarEntry.ID`(§activeShiftOf).
|
|
746
|
+
*
|
|
747
|
+
* **파생값이다** — 델타로 실어 보내지 않는다(교대 경계는 이벤트 없이 시각만으로 넘어간다).
|
|
748
|
+
* 3교대처럼 하루를 끊김 없이 덮는 현장에서 이 값이 **교대별 성과를 물을 수 있는 유일한 축**이다.
|
|
749
|
+
*/
|
|
750
|
+
shift?: string;
|
|
751
|
+
/**
|
|
752
|
+
* 지금 어디에 있나 — **표준 `Person.OperationalLocation`**(B2MML-Personnel.xsd, `ResourceLocationType`).
|
|
753
|
+
*
|
|
754
|
+
* 표준은 사람에게 위치를 준다. 우리 모델에는 없어서 "이 라인에 몇 명 있나" 를 물을 수 없었다.
|
|
755
|
+
* 커널은 사람을 움직이지 않으므로(작업이 사람을 부른다) 이 값은 **마스터가 말해 주거나 관측으로
|
|
756
|
+
* 들어온다** — 그래서 사람 델타에도 자리가 있다(상태⊆이벤트).
|
|
757
|
+
*/
|
|
758
|
+
location?: string;
|
|
759
|
+
/** 자원 속성 — 표준 `PersonProperty`. 자격증·숙련 등급 같은 사실이 여기 들어간다(§ResourceProperty). */
|
|
760
|
+
properties?: ResourceProperty[];
|
|
761
|
+
/**
|
|
762
|
+
* 근무 캘린더 — 표준 `WorkCalendarEntry`(복수). 교대 여러 개와 휴게·휴일을 함께 표현한다.
|
|
763
|
+
* 예전에는 하루에 창 하나뿐이어서 2교대·야간+주간 조합을 표현할 수 없었다.
|
|
764
|
+
*/
|
|
765
|
+
workCalendar?: WorkCalendarEntry[];
|
|
766
|
+
/** 자격을 검증한 시험 명세들 — 표준 `Person.TestSpecificationID`(§TestSpecificationRefs). */
|
|
767
|
+
testSpecificationIds?: TestSpecificationRefs;
|
|
163
768
|
}
|
|
164
769
|
/**
|
|
165
770
|
* 물리 자산 — **ISA-95 `PhysicalAsset`, GS1 `GRAI`(반복사용 자산).**
|
|
@@ -171,10 +776,15 @@ export interface PersonState {
|
|
|
171
776
|
*
|
|
172
777
|
* 그리고 빈 팔레트 부족은 현장의 실제 제약이다 — 자산이 없어 작업이 못 나가는 일이 사람 부족만큼 잦다.
|
|
173
778
|
*/
|
|
174
|
-
export interface AssetState {
|
|
779
|
+
export interface AssetState extends EffectivePeriod {
|
|
175
780
|
id: string;
|
|
176
781
|
/** 자산 등급 — pallet · rack · bin · trailer 등. 도메인 소유(코어는 강제하지 않는다). */
|
|
177
|
-
|
|
782
|
+
/**
|
|
783
|
+
* 속한 등급들 — **표준 `PhysicalAsset.PhysicalAssetClassID`, `maxOccurs="unbounded"`.**
|
|
784
|
+
* 인원과 같은 이유로 복수다(팔레트가 'euro-pallet' 이면서 'food-grade' 일 수 있다).
|
|
785
|
+
* 요구 쪽(`physicalAssetSpecification`)은 표준대로 등급 하나 + 수량이다.
|
|
786
|
+
*/
|
|
787
|
+
assetClassIds?: string[];
|
|
178
788
|
/** 지금 있는 자리. */
|
|
179
789
|
location?: string;
|
|
180
790
|
/** 'idle' | 'in-use'. 고장·OEE 로 평가하지 않는다(설비가 아니다). */
|
|
@@ -186,6 +796,15 @@ export interface AssetState {
|
|
|
186
796
|
* 비어 있으면 빈 팔레트다(회수 대상이자 다음 출고의 재료).
|
|
187
797
|
*/
|
|
188
798
|
carrying?: string;
|
|
799
|
+
/** 자원 속성 — 표준 `PhysicalAssetProperty`(§ResourceProperty). */
|
|
800
|
+
properties?: ResourceProperty[];
|
|
801
|
+
/** 적격을 검증한 시험 명세들 — 표준 `PhysicalAsset.TestSpecificationID`(§TestSpecificationRefs). */
|
|
802
|
+
testSpecificationIds?: TestSpecificationRefs;
|
|
803
|
+
/**
|
|
804
|
+
* 유효 기간 밖 — 설비와 같은 뜻(§Effectivity). 자산에서는 **폐기한 팔레트**가 풀에서 빠지는 것이다.
|
|
805
|
+
* 빠뜨리면 회수 대상 수가 실제보다 많게 잡히고, 빈 팔레트 부족이 보이지 않는다.
|
|
806
|
+
*/
|
|
807
|
+
effectivity?: Effectivity;
|
|
189
808
|
}
|
|
190
809
|
export interface TaskState {
|
|
191
810
|
id: string;
|
|
@@ -200,7 +819,7 @@ export interface TaskState {
|
|
|
200
819
|
/**
|
|
201
820
|
* 남은 시간·총 소요(ms) — 진행 중인 작업을 이어서 굴리는 데 필요(씨앗의 충실도).
|
|
202
821
|
* `remainingMs` 는 **마지막 전이 시점의 값**이다. 델타는 매 tick 오지 않으므로(설계) 미러가 든 값은
|
|
203
|
-
* 그때의 것이고, 지금 값은 `startedAtSimMs` 로 보간한다 — 모션(
|
|
822
|
+
* 그때의 것이고, 지금 값은 `startedAtSimMs` 로 보간한다 — 모션(EquipmentMotion)과 같은 규율.
|
|
204
823
|
*/
|
|
205
824
|
remainingMs?: number;
|
|
206
825
|
durationMs?: number;
|
|
@@ -216,6 +835,26 @@ export interface TaskState {
|
|
|
216
835
|
* 한 작업이 설비 하나와 사람 여럿을 동시에 잡을 수 있다(용접 로봇 1대 + 작업자 2명).
|
|
217
836
|
*/
|
|
218
837
|
personnel?: string[];
|
|
838
|
+
/**
|
|
839
|
+
* 우선순위 — 표준 `JobOrder.Priority`. **작은 값이 급하다**(§PRIORITY_UNSET). 없으면 우선순위 없음.
|
|
840
|
+
*
|
|
841
|
+
* 없을 때는 선착순만 가능했다 — 급한 일을 앞세우는 것이 운영의 기본인데 모델에 자리가 없었다.
|
|
842
|
+
*/
|
|
843
|
+
priority?: number;
|
|
844
|
+
/** 예정 착수 — 표준 `JobOrder.StartTime`. 실제 착수(`startedAtSimMs`)와 다른 축이다. */
|
|
845
|
+
startTime?: ISOTime;
|
|
846
|
+
/** 예정 완료(납기) — 표준 `JobOrder.EndTime`. 없으면 **지연을 정의할 수 없다**(§dueStatusOf). */
|
|
847
|
+
endTime?: ISOTime;
|
|
848
|
+
/**
|
|
849
|
+
* **실제로 들어가고 나온 자재** — ISA-95 `JobResponse.MaterialActual`(`OpMaterialActualType`).
|
|
850
|
+
* 명세(계획)가 아니라 일어난 일이다. 투입 인원·설비와 같은 채널에 실어 실적을 한 곳에서 읽는다.
|
|
851
|
+
*/
|
|
852
|
+
materialActual?: {
|
|
853
|
+
definitionId: string;
|
|
854
|
+
use: 'consumed' | 'produced';
|
|
855
|
+
quantity: number;
|
|
856
|
+
uom?: string;
|
|
857
|
+
}[];
|
|
219
858
|
}
|
|
220
859
|
export interface OrderState {
|
|
221
860
|
id: string;
|
|
@@ -235,6 +874,12 @@ export interface OrderState {
|
|
|
235
874
|
requested?: number;
|
|
236
875
|
fulfilled?: number;
|
|
237
876
|
lines?: ObservedOrderLine[];
|
|
877
|
+
/** 우선순위 — 표준 `OperationsRequest.Priority`. 작은 값이 급하다. 오더 할당 순서를 정한다. */
|
|
878
|
+
priority?: number;
|
|
879
|
+
/** 예정 착수 — 표준 `OperationsRequest.StartTime`. */
|
|
880
|
+
startTime?: ISOTime;
|
|
881
|
+
/** 납기 — 표준 `OperationsRequest.EndTime`. 이것이 있어야 "늦었나" 를 물을 수 있다. */
|
|
882
|
+
endTime?: ISOTime;
|
|
238
883
|
}
|
|
239
884
|
export type AttentionSeverity = 'low' | 'medium' | 'high' | 'critical';
|
|
240
885
|
export type AttentionState = 'active' | 'acknowledged' | 'cleared';
|
|
@@ -243,16 +888,39 @@ export interface Attention {
|
|
|
243
888
|
kind: string;
|
|
244
889
|
severity: AttentionSeverity;
|
|
245
890
|
state?: AttentionState;
|
|
891
|
+
/**
|
|
892
|
+
* 이 신호가 가리키는 대상 — 공간 위 위치로 해석한다.
|
|
893
|
+
*
|
|
894
|
+
* `operation` 은 개체가 아니라 **공정(단계)** 이다. 개체 셋(자리·설비·오더)만으로는 "도장이라는
|
|
895
|
+
* 단계가 라인의 제약이다" 를 가리킬 수 없다 — 도장 부스는 여럿이고 그중 하나가 문제인 게 아니라
|
|
896
|
+
* **그 단계 전체**가 모자란 것이다. 표준에도 자리가 있다(ISA-95 `OperationsSegment`).
|
|
897
|
+
*/
|
|
246
898
|
anchor: {
|
|
247
|
-
|
|
899
|
+
locationId?: string;
|
|
248
900
|
moverId?: string;
|
|
249
901
|
orderId?: string;
|
|
902
|
+
operation?: string;
|
|
250
903
|
};
|
|
251
|
-
|
|
904
|
+
/**
|
|
905
|
+
* **이 조건이 언제부터인가** — 표준 `WorkAlertType.TimeStamp`(ISO 절대 시각).
|
|
906
|
+
*
|
|
907
|
+
* ── 죽은 필드였다 ────────────────────────────────────────────────────────
|
|
908
|
+
* 예전 이름은 `since`(simClockMs)였는데 **한 번도 채워지지 않았고 아무도 읽지 않았다.** 그래서
|
|
909
|
+
* 사용자는 병목이 **10초째인지 3시간째인지** 구별할 수 없었다 — 방금 찬 자리와 세 시간째 막힌 자리는
|
|
910
|
+
* 할 일이 완전히 다르다. 표준이 알림에 시각을 두는 이유가 그것이다.
|
|
911
|
+
*
|
|
912
|
+
* **재평가 시각이 아니라 조건이 처음 성립한 시각**이다. 주목 신호는 매 스냅샷 다시 계산되므로,
|
|
913
|
+
* "지금" 을 찍으면 언제나 방금 생긴 것처럼 보인다(그 값은 스냅샷 시각과 같아 아무 정보가 없다).
|
|
914
|
+
* 그래서 커널이 **처음 본 시각을 기억**한다(조건이 사라지면 함께 지운다 — 확인(ack)과 같은 수명).
|
|
915
|
+
*
|
|
916
|
+
* 시뮬 클록(ms)이 아니라 **절대 시각**이다: 소비처가 화면에 "3시간째" 를 쓰려면 기준이 필요하고,
|
|
917
|
+
* 라이브에서는 시뮬 클록이 아예 뜻이 없다.
|
|
918
|
+
*/
|
|
919
|
+
timeStamp?: ISOTime;
|
|
252
920
|
/**
|
|
253
921
|
* 표현용 원시 파라미터(언어 중립). 커널은 사람이 읽는 문장을 만들지 않는다 — kind + params 만 방출하고
|
|
254
922
|
* title/detail/rationale 는 표현계층(클라 i18next 템플릿)이 kind 로 키를 골라 params 를 보간해 렌더.
|
|
255
|
-
* 예: bottleneck → {
|
|
923
|
+
* 예: bottleneck → { locationId, occupancy, capacity, ratioPct, saturated }.
|
|
256
924
|
*/
|
|
257
925
|
params?: Record<string, string | number>;
|
|
258
926
|
recommendedActions?: RecommendedAction[];
|
|
@@ -275,9 +943,27 @@ export interface RecommendedAction {
|
|
|
275
943
|
export interface StateSnapshot {
|
|
276
944
|
revision: number;
|
|
277
945
|
simClockMs: number;
|
|
278
|
-
|
|
946
|
+
/**
|
|
947
|
+
* **이 트윈의 "지금"**(ISO 절대 시각) — 시각으로 재는 모든 판단의 기준.
|
|
948
|
+
*
|
|
949
|
+
* `simClockMs` 로는 부족하다: 소비처가 절대 시각을 얻으려면 **기준 epoch 를 복제**해야 하고,
|
|
950
|
+
* 관측(라이브) 모드에서는 그 계산이 아예 틀린다(그때의 "지금" 은 마지막으로 들은 발생 시각이다).
|
|
951
|
+
* 트윈은 자기 시계로 사니, **그 시계를 트윈이 직접 말한다**.
|
|
952
|
+
*
|
|
953
|
+
* 없으면 소비처는 **재지 않는다** — 벽시계로 대신 재면 시뮬 트윈에서 엉뚱한 값이 나온다.
|
|
954
|
+
*/
|
|
955
|
+
nowTime?: ISOTime;
|
|
956
|
+
locations: LocationState[];
|
|
279
957
|
items: ItemState[];
|
|
280
|
-
|
|
958
|
+
/**
|
|
959
|
+
* 설비 — **ISA-95 `Equipment`.** 고정 설비(도장기·용접로봇)와 이동 설비(지게차·호슬러)를 한 그릇에
|
|
960
|
+
* 든다. 둘의 상태 모델이 같기 때문이다(고장·가동·교대·계획정지·작업 점유). 갈리는 것은 하나뿐 —
|
|
961
|
+
* 소속(`homeLocation`)과 현재 위치(`location`)가 같은지.
|
|
962
|
+
*
|
|
963
|
+
* 예전 이름은 `movers` 였다(씬의 움직임 믹스인에서 물려받은 것). 커널에는 애니메이션이 없고 (vocabulary-guard: allow — 개명 경위 서술)
|
|
964
|
+
* 도장 부스는 아무것도 옮기지 않으므로 그 이름은 거짓이었다. 표준이 이미 정한 이름을 쓴다.
|
|
965
|
+
*/
|
|
966
|
+
equipment: EquipmentState[];
|
|
281
967
|
/** 사람 — 등급·교대·투입 상태. 인원을 선언하지 않은 트윈에서는 빈 배열. */
|
|
282
968
|
persons: PersonState[];
|
|
283
969
|
/** 물리 자산(반복사용) — 선언하지 않은 트윈에서는 빈 배열. */
|
|
@@ -352,11 +1038,20 @@ export interface GeneratorSpec {
|
|
|
352
1038
|
kind: string;
|
|
353
1039
|
/**
|
|
354
1040
|
* 코어 라우팅 클래스: 공급(arrival, 물건이 들어옴 → onArrival) vs 수요(order, 요청이 들어옴 → onOrder).
|
|
355
|
-
* 미지정 시 레거시 kind('outbound-order'→order, 그 외→arrival)로 추론(하위호환, 카탈로그
|
|
1041
|
+
* 미지정 시 레거시 kind('outbound-order'→order, 그 외→arrival)로 추론(하위호환, 카탈로그 locationTypes 폴백과 동형).
|
|
356
1042
|
*/
|
|
357
1043
|
stimulus?: 'arrival' | 'order';
|
|
358
1044
|
rate: RateSpec;
|
|
359
1045
|
content: ContentSpec;
|
|
1046
|
+
/**
|
|
1047
|
+
* 이 자극이 만드는 오더의 **약속 리드타임(분)** — 생성 시각 + 이 값이 납기(`endTime`)가 된다.
|
|
1048
|
+
*
|
|
1049
|
+
* 표준 `OperationsRequest.EndTime` 을 시나리오가 채우는 경로다. **선언하지 않으면 납기가 없다** —
|
|
1050
|
+
* 그러면 `dueStatusOf` 가 판단하지 않고 지연 신호도 뜨지 않는다(없는 약속을 만들지 않는다).
|
|
1051
|
+
*/
|
|
1052
|
+
promisedLeadMinutes?: number;
|
|
1053
|
+
/** 이 자극이 만드는 오더의 우선순위 — 표준 `OperationsRequest.Priority`(작은 값이 급하다). */
|
|
1054
|
+
priority?: number;
|
|
360
1055
|
/**
|
|
361
1056
|
* 운영시간 — 이 구간 밖에서는 자극이 발생하지 않는다(문 닫은 시간에 트럭이 오지 않는다).
|
|
362
1057
|
* `startHour <= endHour` 면 같은 날 구간, 넘어가면 자정을 가로지르는 야간 구간(22→6).
|
|
@@ -391,23 +1086,25 @@ export declare const OP_EVENT: {
|
|
|
391
1086
|
readonly quality: "quality.output";
|
|
392
1087
|
};
|
|
393
1088
|
/** 사람 상태 델타 — 배정·해제·교대 전이 시 방출. 미러가 인원 가용을 비추는 근거. */
|
|
394
|
-
export interface PersonStatusDelta {
|
|
1089
|
+
export interface PersonStatusDelta extends EffectivePeriod {
|
|
395
1090
|
personId: string;
|
|
396
|
-
|
|
1091
|
+
personnelClassIds?: string[];
|
|
397
1092
|
status: string;
|
|
398
1093
|
taskId?: string;
|
|
399
1094
|
offShift?: boolean;
|
|
1095
|
+
/** 지금 어디에 있나 — 나가지 않으면 미러가 사람의 위치를 영영 모른다(상태⊆이벤트). */
|
|
1096
|
+
location?: string;
|
|
400
1097
|
}
|
|
401
1098
|
/** 물리 자산 상태 델타 — 이동·투입·적재/하역 시 방출. 미러가 자산 가용을 비추는 근거. */
|
|
402
|
-
export interface AssetStatusDelta {
|
|
1099
|
+
export interface AssetStatusDelta extends EffectivePeriod {
|
|
403
1100
|
assetId: string;
|
|
404
|
-
|
|
1101
|
+
assetClassIds?: string[];
|
|
405
1102
|
status: string;
|
|
406
1103
|
location?: string;
|
|
407
1104
|
taskId?: string;
|
|
408
1105
|
carrying?: string;
|
|
409
1106
|
}
|
|
410
|
-
/** 품질 산출 델타 — recordOutput(양품/불량) 시 방출. goodCount/scrapCount 는
|
|
1107
|
+
/** 품질 산출 델타 — recordOutput(양품/불량) 시 방출. goodCount/scrapCount 는 설비 누적값. */
|
|
411
1108
|
export interface QualityDelta {
|
|
412
1109
|
moverId: string;
|
|
413
1110
|
good: boolean;
|
|
@@ -451,15 +1148,40 @@ export interface TaskStatusDelta {
|
|
|
451
1148
|
remainingMs?: number;
|
|
452
1149
|
/** 이 작업의 총 소요 예상(ms) — 진척의 분모. */
|
|
453
1150
|
durationMs?: number;
|
|
1151
|
+
/** 우선순위 — 나가지 않으면 미러가 배정 순서의 근거를 모른다(상태⊆이벤트). */
|
|
1152
|
+
priority?: number;
|
|
1153
|
+
startTime?: ISOTime;
|
|
1154
|
+
endTime?: ISOTime;
|
|
1155
|
+
/**
|
|
1156
|
+
* **실제로 들어가고 나온 자재** — ISA-95 `JobResponse.MaterialActual`(`OpMaterialActualType`).
|
|
1157
|
+
* 명세(계획)가 아니라 일어난 일이다. 투입 인원·설비와 같은 채널에 실어 실적을 한 곳에서 읽는다.
|
|
1158
|
+
*/
|
|
1159
|
+
materialActual?: {
|
|
1160
|
+
definitionId: string;
|
|
1161
|
+
use: 'consumed' | 'produced';
|
|
1162
|
+
quantity: number;
|
|
1163
|
+
uom?: string;
|
|
1164
|
+
}[];
|
|
454
1165
|
}
|
|
455
|
-
export interface EquipmentStatusDelta {
|
|
1166
|
+
export interface EquipmentStatusDelta extends EffectivePeriod {
|
|
456
1167
|
moverId: string;
|
|
457
1168
|
kind: string;
|
|
458
1169
|
status: string;
|
|
459
1170
|
location?: string;
|
|
1171
|
+
/** 붙박인 자리(`EquipmentState.homeLocation`). 나가지 않으면 미러가 소속을 영영 모른다 — 상태⊆이벤트. */
|
|
1172
|
+
homeLocation?: string;
|
|
460
1173
|
/** 지금 붙어 있는 작업 — 사람·자산 델타와 같은 자리. 없으면 미러가 작업↔자원 연결을 모른다. */
|
|
461
1174
|
taskId?: string;
|
|
462
|
-
|
|
1175
|
+
/**
|
|
1176
|
+
* 계획 정지(정비·오프라인) — **커맨드로만 바뀌는 사실**이므로 델타로 실어 보내는 것이 맞다
|
|
1177
|
+
* (시각으로 바뀌는 판정과 다르다: 여기엔 항상 이벤트가 있다).
|
|
1178
|
+
*
|
|
1179
|
+
* 없어서 미러는 계획 정지를 **영영 몰랐다.** 그런데 적합성 하네스는 조용했다 — 대조가 값 있는 필드만
|
|
1180
|
+
* 세는데 아무도 정지 상태가 아닌 실행에서는 양쪽 다 비어 있었기 때문이다. 그래서 하네스가 실행 중간에
|
|
1181
|
+
* **조건을 일부러 일으키게** 고치고(`provoke`), 그 눈으로 이 구멍을 찾았다.
|
|
1182
|
+
*/
|
|
1183
|
+
held?: boolean;
|
|
1184
|
+
motion?: EquipmentMotion;
|
|
463
1185
|
}
|
|
464
1186
|
/** 관측된 오더 라인(SKU 데맨드) — 실 시스템 오더는 품목 라인을 가짐. 이행 예측(남은 데맨드 재계획)에 필요. */
|
|
465
1187
|
export interface ObservedOrderLine {
|
|
@@ -479,6 +1201,10 @@ export interface OrderStatusDelta {
|
|
|
479
1201
|
* 현재 재고로 어떻게 채우나"를 재계획할 수 있다. 없으면 top-level 카운트만(이행 예측 불가, 재고 예측만).
|
|
480
1202
|
*/
|
|
481
1203
|
lines?: ObservedOrderLine[];
|
|
1204
|
+
/** 우선순위·예정 창 — 미러가 "늦었나" 를 판단할 재료. */
|
|
1205
|
+
priority?: number;
|
|
1206
|
+
startTime?: ISOTime;
|
|
1207
|
+
endTime?: ISOTime;
|
|
482
1208
|
}
|
|
483
1209
|
export declare const CMD: {
|
|
484
1210
|
readonly orderHold: "order.hold";
|
|
@@ -495,9 +1221,37 @@ export declare const CMD: {
|
|
|
495
1221
|
export type OperationalDelta = TaskStatusDelta | EquipmentStatusDelta | PersonStatusDelta | AssetStatusDelta | OrderStatusDelta;
|
|
496
1222
|
export type EventHandler = (e: CanonicalEnvelope) => void;
|
|
497
1223
|
export type Unsubscribe = () => void;
|
|
1224
|
+
/**
|
|
1225
|
+
* **저장된 보드를 읽는 단 하나의 입구.**
|
|
1226
|
+
*
|
|
1227
|
+
* `equipment` 로 개명하기 전에 저장된 보드는 `movers` 키를 갖고 있다(개명 시점 23개 인스턴스). (vocabulary-guard: allow — 읽기 호환 설명)
|
|
1228
|
+
* 저장물을 다시 쓰지 않고 **읽을 때 흡수**한다 — 마이그레이션은 되돌리기 어렵고, 읽기 호환은 값싸다.
|
|
1229
|
+
*
|
|
1230
|
+
* 규율 둘:
|
|
1231
|
+
* - 이 함수를 **거치지 않고** `def.equipment` 를 직접 읽는 코드를 두지 않는다. 하나라도 남으면
|
|
1232
|
+
* 그 경로에서만 옛 보드의 설비가 조용히 사라진다(빈 배열).
|
|
1233
|
+
* - **쓸 때는 새 이름만** 쓴다. 두 이름으로 쓰기 시작하면 저장물에 두 벌이 영구히 섞인다.
|
|
1234
|
+
*
|
|
1235
|
+
* 제거 시점: 저장된 보드가 모두 `equipment` 키로 바뀐 것이 확인되면(운영 데이터 점검 후) 이 함수는
|
|
1236
|
+
* 사라진다. 그때까지 남겨 두는 이유를 여기 적어 두는 것이 주석의 일이다.
|
|
1237
|
+
*/
|
|
1238
|
+
/**
|
|
1239
|
+
* **저장된 보드의 자리를 읽는 단 하나의 입구.** `readBoardEquipment` 와 같은 규율.
|
|
1240
|
+
*
|
|
1241
|
+
* `nodes` → `locations` 개명(2026-08-01) 전에 저장된 보드는 `nodes` 키를 갖고 있다(개명 시점 23개). (vocabulary-guard: allow — 읽기 호환 설명)
|
|
1242
|
+
* 이 함수를 거치지 않고 `def.locations` 를 직접 읽는 코드를 두지 않는다 — 하나라도 남으면 그 경로에서만
|
|
1243
|
+
* 옛 보드의 자리가 조용히 사라진다(빈 배열 = 자리 없는 트윈 = 아무 일도 일어나지 않는다).
|
|
1244
|
+
*/
|
|
1245
|
+
export declare function readBoardLocations(def: BoardDef | (Record<string, unknown> & {
|
|
1246
|
+
locations?: unknown;
|
|
1247
|
+
nodes?: unknown;
|
|
1248
|
+
})): BoardDef['locations'];
|
|
1249
|
+
export declare function readBoardEquipment(def: BoardDef | Record<string, unknown>): BoardDef['equipment'];
|
|
1250
|
+
/** 저장된 보드의 반복사용 자산 — 설비와 같은 정규화를 거친다. */
|
|
1251
|
+
export declare function readBoardAssets(def: BoardDef | Record<string, unknown>): NonNullable<BoardDef['assets']>;
|
|
498
1252
|
export interface BoardDef {
|
|
499
|
-
/** parallelism = 동시 처리 수(
|
|
500
|
-
|
|
1253
|
+
/** parallelism = 동시 처리 수(LocationState.parallelism 참조). capacity 는 저장 용량. */
|
|
1254
|
+
locations: {
|
|
501
1255
|
id: string;
|
|
502
1256
|
type: string;
|
|
503
1257
|
capacity: number;
|
|
@@ -505,38 +1259,75 @@ export interface BoardDef {
|
|
|
505
1259
|
parentId?: string;
|
|
506
1260
|
}[];
|
|
507
1261
|
/**
|
|
508
|
-
*
|
|
1262
|
+
* 설비(설비). mtbfMs/mttrMs 지정 시 확률적 고장 모델 참여(OEE Availability 손실). 미지정=고장 없음.
|
|
509
1263
|
* `window` 지정 시 그 시간대에만 일한다(교대·가동시간) — 미지정이면 24시간 가용(기존 거동).
|
|
510
1264
|
*/
|
|
511
|
-
|
|
1265
|
+
equipment: (EffectivePeriod & {
|
|
512
1266
|
id: string;
|
|
513
1267
|
kind: string;
|
|
514
|
-
|
|
1268
|
+
homeLocation: string;
|
|
515
1269
|
mtbfMs?: number;
|
|
516
1270
|
mttrMs?: number;
|
|
517
1271
|
window?: {
|
|
518
1272
|
startHour: number;
|
|
519
1273
|
endHour: number;
|
|
520
1274
|
};
|
|
521
|
-
|
|
1275
|
+
workCalendar?: WorkCalendarEntry[];
|
|
1276
|
+
properties?: ResourceProperty[];
|
|
1277
|
+
testSpecificationIds?: TestSpecificationRefs;
|
|
1278
|
+
})[];
|
|
522
1279
|
/**
|
|
523
|
-
* 사람 — ISA-95 `Person`. `
|
|
1280
|
+
* 사람 — ISA-95 `Person`. `personnelClasses` 로 **속한 등급들**을 밝히고(복수가 표준), `window` 로
|
|
1281
|
+
* 교대를 선언한다.
|
|
524
1282
|
* 선언하지 않으면 인원 제약이 없는 트윈이다(기존 거동).
|
|
525
1283
|
*/
|
|
526
|
-
persons?: {
|
|
1284
|
+
persons?: (EffectivePeriod & {
|
|
527
1285
|
id: string;
|
|
528
|
-
|
|
1286
|
+
personnelClassIds?: string[];
|
|
529
1287
|
window?: {
|
|
530
1288
|
startHour: number;
|
|
531
1289
|
endHour: number;
|
|
532
1290
|
};
|
|
533
|
-
|
|
1291
|
+
workCalendar?: WorkCalendarEntry[];
|
|
1292
|
+
homeLocation?: string;
|
|
1293
|
+
properties?: ResourceProperty[];
|
|
1294
|
+
testSpecificationIds?: TestSpecificationRefs;
|
|
1295
|
+
})[];
|
|
534
1296
|
/** 물리 자산(반복사용) — ISA-95 `PhysicalAsset` / GS1 `GRAI`. 선언하지 않으면 자산 제약이 없다. */
|
|
535
|
-
assets?: {
|
|
1297
|
+
assets?: (EffectivePeriod & {
|
|
536
1298
|
id: string;
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
1299
|
+
assetClassIds?: string[];
|
|
1300
|
+
homeLocation?: string;
|
|
1301
|
+
properties?: ResourceProperty[];
|
|
1302
|
+
testSpecificationIds?: TestSpecificationRefs;
|
|
1303
|
+
})[];
|
|
1304
|
+
/**
|
|
1305
|
+
* 이 트윈의 **시각 해석 기준**(UTC 로부터의 분). 근무 캘린더의 `HH:MM` 이 어느 기준인지 정한다.
|
|
1306
|
+
*
|
|
1307
|
+
* 예: Rosarito(UTC−7) = `-420`. **선언하지 않으면 UTC**(0)로 읽는다 — 조용히 현지 시각으로
|
|
1308
|
+
* 가정하지 않는다. 테넌트 시간대는 호스트가 안다(`Domain.timezone`) — 그것을 오프셋으로 풀어 준다.
|
|
1309
|
+
*
|
|
1310
|
+
* 일광절약시간은 고정 오프셋으로 따라갈 수 없다. 긴 지평선의 정확한 답은 호스트가 표준대로
|
|
1311
|
+
* **절대 구간**(`StartDateTime`/`FinishDateTime`)을 계산해 넣는 것이다.
|
|
1312
|
+
*/
|
|
1313
|
+
utcOffsetMinutes?: number;
|
|
1314
|
+
/**
|
|
1315
|
+
* **등급 정의** — 표준 `PersonnelClass` · `EquipmentClass` · `PhysicalAssetClass`.
|
|
1316
|
+
*
|
|
1317
|
+
* 선언하지 않아도 트윈은 돈다(소속 문자열 그대로 판정). 선언하면 **상속과 유효기간**이 살아난다 —
|
|
1318
|
+
* "생산직 2명" 요구를 "용접 자격자" 가 만족하고, 만료된 자격은 배정되지 않는다.
|
|
1319
|
+
*/
|
|
1320
|
+
/**
|
|
1321
|
+
* **품목 정의**(표준 `MaterialDefinition`)와 **품목 등급**(`MaterialClass`).
|
|
1322
|
+
*
|
|
1323
|
+
* 선언하지 않아도 트윈은 돈다 — 그때는 단위 환산을 할 수 없고, `quantityIn` 이 **없다고 답한다**
|
|
1324
|
+
* (계수를 모르는데 값을 만들면 그 뒤 모든 계산이 거짓 위에 선다).
|
|
1325
|
+
*/
|
|
1326
|
+
materialDefinitions?: MaterialDefinition[];
|
|
1327
|
+
materialClasses?: ResourceClassDef[];
|
|
1328
|
+
personnelClasses?: ResourceClassDef[];
|
|
1329
|
+
equipmentClasses?: ResourceClassDef[];
|
|
1330
|
+
assetClasses?: ResourceClassDef[];
|
|
540
1331
|
}
|
|
541
1332
|
export interface TwinKernel {
|
|
542
1333
|
loadBoard(def: BoardDef): void;
|