@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.js
CHANGED
|
@@ -5,22 +5,488 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* 자리의 상태 — **포화도에서 파생한다.** 저장하는 값이 아니다.
|
|
7
7
|
*
|
|
8
|
-
* 예전에는 시뮬이 `'idle'` 로 두고 한 번도 바꾸지 않았고(변경 지점 0), 미러에는
|
|
8
|
+
* 예전에는 시뮬이 `'idle'` 로 두고 한 번도 바꾸지 않았고(변경 지점 0), 미러에는 자리 상태 채널이
|
|
9
9
|
* 없어 비어 있었다. 화면은 그 값을 그대로 보여 주고 있었다 — **정보처럼 보이는데 정보가 아니었다.**
|
|
10
10
|
*
|
|
11
11
|
* 문턱은 병목 주목(`deriveAttentions`)이 쓰는 것과 **같다**: 90% 이상이면 임박, 100% 이상이면 포화.
|
|
12
12
|
* 규칙이 둘이면 화면과 주목이 다른 말을 한다. 용량을 모르면 상태도 모른다(undefined — 꾸미지 않는다).
|
|
13
13
|
*/
|
|
14
|
-
export const
|
|
15
|
-
export function
|
|
14
|
+
export const LOCATION_SATURATION_NEAR = 0.9;
|
|
15
|
+
export function locationStatusOf(n) {
|
|
16
16
|
const cap = n.capacity;
|
|
17
17
|
if (!(typeof cap === 'number' && cap > 0))
|
|
18
18
|
return undefined;
|
|
19
19
|
const r = (n.occupancy ?? 0) / cap;
|
|
20
|
-
return r >= 1 ? 'full' : r >=
|
|
20
|
+
return r >= 1 ? 'full' : r >= LOCATION_SATURATION_NEAR ? 'near-full' : 'available';
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 설비 계층 단계 — **ISA-95 표준 어휘.** 1차 출처: B2MML `B2MML-Common.xsd` /
|
|
24
|
+
* `EquipmentLevel1Type` 열거값 + `EquipmentLevelType` 주석("role based equipment hierarchy level
|
|
25
|
+
* as defined in ISA 95").
|
|
26
|
+
*
|
|
27
|
+
* 우리가 단의 이름을 발명하지 않는다. "라인" 은 `ProductionLine`, "존" 은 `StorageZone` 으로
|
|
28
|
+
* 표준이 이미 정해 뒀다. 발명하면 그 순간 방언이 되고, 연동 상대와 매핑 표가 필요해진다.
|
|
29
|
+
*
|
|
30
|
+
* **`Other` 는 탈출구다** — 표준도 열거값 밖을 인정한다(`OtherValue` 속성). 억지로 끼워 맞추는 대신
|
|
31
|
+
* `Other` 로 두고 현장의 낱말은 `type` 에 남긴다.
|
|
32
|
+
*
|
|
33
|
+
* `StorageZone`·`StorageUnit` 도 이 계층 안에 있다. 다만 **자리 자체는 다른 축**이다 —
|
|
34
|
+
* ISA-95 는 `OperationalLocation`("자원이 놓이거나 놓일 것으로 예상되는 논리적·물리적 장소",
|
|
35
|
+
* `B2MML-OperationalLocation.xsd`)을 별도 스키마로 두고, `Equipment` 가 자기 위치를 그것으로 가리킨다.
|
|
36
|
+
* 우리 `locations` 가 그 개념이다(2026-08-01 개명 — `plans/isa95-coverage.md` §3-1).
|
|
37
|
+
*/
|
|
38
|
+
export const EQUIPMENT_LEVEL = [
|
|
39
|
+
'Enterprise',
|
|
40
|
+
'Site',
|
|
41
|
+
'Area',
|
|
42
|
+
'ProcessCell',
|
|
43
|
+
'Unit',
|
|
44
|
+
'ProductionLine',
|
|
45
|
+
'WorkCell',
|
|
46
|
+
'ProductionUnit',
|
|
47
|
+
'StorageZone',
|
|
48
|
+
'StorageUnit',
|
|
49
|
+
'WorkCenter',
|
|
50
|
+
'WorkUnit',
|
|
51
|
+
'EquipmentModule',
|
|
52
|
+
'ControlModule',
|
|
53
|
+
'Other'
|
|
54
|
+
];
|
|
55
|
+
/** 표준 열거값인지 — 상류에서 들어온 값을 조용히 통과시키지 않고 확인하는 용도. */
|
|
56
|
+
export function isEquipmentLevel(v) {
|
|
57
|
+
return typeof v === 'string' && EQUIPMENT_LEVEL.includes(v);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 계층 색인을 만든다. **순환은 만들 때 잡고 던진다** — 렌더 도중에 터지는 대신 여기서 한 번에.
|
|
61
|
+
* 순환을 조용히 잘라 내면 롤업이 틀린 값을 내고, 그건 이 함수가 막으려는 바로 그 실패다.
|
|
62
|
+
*/
|
|
63
|
+
export function hierarchyOf(s) {
|
|
64
|
+
const push = (m, k, v) => {
|
|
65
|
+
const cur = m.get(k);
|
|
66
|
+
if (cur)
|
|
67
|
+
cur.push(v);
|
|
68
|
+
else
|
|
69
|
+
m.set(k, [v]);
|
|
70
|
+
};
|
|
71
|
+
const parent = new Map();
|
|
72
|
+
const kids = new Map();
|
|
73
|
+
const known = new Set(s.locations.map(n => n.id));
|
|
74
|
+
for (const n of s.locations) {
|
|
75
|
+
if (!n.parentId || n.parentId === n.id)
|
|
76
|
+
continue; // 자기 부모는 선언 오류 — 사슬에 넣지 않는다
|
|
77
|
+
parent.set(n.id, n.parentId);
|
|
78
|
+
if (known.has(n.parentId))
|
|
79
|
+
push(kids, n.parentId, n.id);
|
|
80
|
+
}
|
|
81
|
+
/* 순환 검출 — 자리마다 사슬을 끝까지 밀어 본다. 방문한 자리를 다시 만나면 그 경로를 그대로 알린다. */
|
|
82
|
+
for (const start of known) {
|
|
83
|
+
const seen = [start];
|
|
84
|
+
for (let at = parent.get(start); at !== undefined; at = parent.get(at)) {
|
|
85
|
+
if (seen.includes(at))
|
|
86
|
+
throw new Error(`location hierarchy has a cycle: ${[...seen, at].join(' → ')}`);
|
|
87
|
+
seen.push(at);
|
|
88
|
+
if (!known.has(at))
|
|
89
|
+
break; // 구역에 닿았다 — 사슬 끝
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const homes = new Map();
|
|
93
|
+
for (const m of s.equipment ?? [])
|
|
94
|
+
if (m.homeLocation)
|
|
95
|
+
push(homes, m.homeLocation, m.id);
|
|
96
|
+
const ancestorsOf = (id) => {
|
|
97
|
+
const out = [];
|
|
98
|
+
for (let at = parent.get(id); at !== undefined; at = parent.get(at)) {
|
|
99
|
+
out.push(at);
|
|
100
|
+
if (!known.has(at))
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
};
|
|
105
|
+
const descendantsOf = (id) => {
|
|
106
|
+
const out = [];
|
|
107
|
+
const stack = [...(kids.get(id) ?? [])];
|
|
108
|
+
while (stack.length) {
|
|
109
|
+
const cur = stack.pop();
|
|
110
|
+
out.push(cur);
|
|
111
|
+
stack.push(...(kids.get(cur) ?? []));
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
};
|
|
115
|
+
const typeOf = new Map(s.locations.map(n => [n.id, n.type]));
|
|
116
|
+
const levelOf = new Map(s.locations.map(n => [n.id, n.level]));
|
|
117
|
+
return {
|
|
118
|
+
childrenOf: id => [...(kids.get(id) ?? [])],
|
|
119
|
+
ancestorsOf,
|
|
120
|
+
descendantsOf,
|
|
121
|
+
rollupOf: id => ancestorsOf(id).find(a => !known.has(a)),
|
|
122
|
+
ancestorOfLevel: (id, level) => ancestorsOf(id).find(a => known.has(a) && levelOf.get(a) === level),
|
|
123
|
+
ancestorOfType: (id, type) => ancestorsOf(id).find(a => known.has(a) && typeOf.get(a) === type),
|
|
124
|
+
equipmentOf: (id, opts) => {
|
|
125
|
+
const scope = opts?.deep ? [id, ...descendantsOf(id)] : [id];
|
|
126
|
+
return scope.flatMap(n => homes.get(n) ?? []);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* 이 시각에 유효 기간 밖인가 — **한 규칙**으로 개체·등급·설비↔자산 매핑을 모두 판정한다.
|
|
132
|
+
*
|
|
133
|
+
* **표준은 날짜만 정하고 "밖이면 어떻게 되는가" 는 정하지 않는다.** 그 판단은 우리 것이므로 여기 밝힌다:
|
|
134
|
+
* 기간 밖이면 **그 자원은 그 시각의 모델에 참여하지 않는다**(배정되지 않고, 가용 분모에 들지 않는다).
|
|
135
|
+
* 지우지는 않는다 — 이유를 달아 남긴다. 조용히 사라지면 "왜 없어졌나" 를 아무도 답할 수 없다.
|
|
136
|
+
*
|
|
137
|
+
* `at` 를 주지 않으면 **판단하지 않는다**(`undefined`). 모르는 시각으로 폐기를 단정하면, 시각을 안 넘긴
|
|
138
|
+
* 소비처 전부가 자원을 잃는다. 파싱 불가한 시각도 같다 — 짐작해 고치지 않는다.
|
|
139
|
+
*/
|
|
140
|
+
export function effectivityAt(p, at) {
|
|
141
|
+
if (!p || !at)
|
|
142
|
+
return undefined;
|
|
143
|
+
const atMs = Date.parse(at);
|
|
144
|
+
if (!Number.isFinite(atMs))
|
|
145
|
+
return undefined;
|
|
146
|
+
const from = p.effectiveStart ? Date.parse(p.effectiveStart) : NaN;
|
|
147
|
+
if (Number.isFinite(from) && atMs < from)
|
|
148
|
+
return 'not-yet';
|
|
149
|
+
const to = p.effectiveEnd ? Date.parse(p.effectiveEnd) : NaN;
|
|
150
|
+
if (Number.isFinite(to) && atMs > to)
|
|
151
|
+
return 'expired';
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* 등급 소속을 **상속을 타고 닫는다** — "이 개체가 이 등급으로 통하는가".
|
|
156
|
+
*
|
|
157
|
+
* 순환은 방문 집합으로 끊는다(잘못된 마스터가 무한 루프를 만들지 않게). 등급 정의가 없으면 소속
|
|
158
|
+
* 그대로만 본다 — 정의를 요구하지 않는다(정의를 싣지 않은 트윈이 그대로 돌아야 한다).
|
|
159
|
+
*
|
|
160
|
+
* `at` 를 주면 **유효 기간 밖의 등급은 제외**한다. 안 주면 기간을 보지 않는다(모르면 판단하지 않는다).
|
|
161
|
+
*/
|
|
162
|
+
export function classClosure(directIds, defs, at) {
|
|
163
|
+
const byId = new Map((defs ?? []).map(d => [d.id, d]));
|
|
164
|
+
const inWindow = (d) => !d || effectivityAt(d, at) === undefined;
|
|
165
|
+
const out = new Set();
|
|
166
|
+
const stack = [...(directIds ?? [])];
|
|
167
|
+
while (stack.length) {
|
|
168
|
+
const id = stack.pop();
|
|
169
|
+
if (out.has(id))
|
|
170
|
+
continue;
|
|
171
|
+
const def = byId.get(id);
|
|
172
|
+
if (!inWindow(def))
|
|
173
|
+
continue; // 만료된 등급은 자기도, 그 상위도 타지 않는다
|
|
174
|
+
out.add(id);
|
|
175
|
+
for (const b of def?.baseIds ?? [])
|
|
176
|
+
if (!out.has(b))
|
|
177
|
+
stack.push(b);
|
|
178
|
+
}
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* 우선순위 — **ISA-95 `Priority`**(`JobOrderType`·`OperationsRequestType`, 타입은 `PriorityType` =
|
|
183
|
+
* `NumericType` 제한). 즉 표준은 **숫자라는 것만 정하고 방향은 정하지 않는다.**
|
|
184
|
+
*
|
|
185
|
+
* **그래서 방향은 우리가 정한다: 작은 값이 급하다(1 = 가장 급함).** 흔한 관행이고, 무엇보다
|
|
186
|
+
* 한쪽으로 못 박아 두지 않으면 소비처마다 반대로 읽는다. 우리가 정한 규약이라는 사실을 여기 밝힌다.
|
|
187
|
+
*
|
|
188
|
+
* 미지정은 **0 이 아니라 "우선순위 없음"** 이다 — 선언한 것들 뒤에 선다(0 으로 채우면 미지정이
|
|
189
|
+
* 가장 급한 것이 된다).
|
|
190
|
+
*/
|
|
191
|
+
export const PRIORITY_UNSET = Number.POSITIVE_INFINITY;
|
|
192
|
+
/** 정렬 키 — 미지정을 맨 뒤로 보낸다. 같은 우선순위는 **입력 순서**를 지킨다(결정성). */
|
|
193
|
+
export function priorityRank(p) {
|
|
194
|
+
return typeof p === 'number' && Number.isFinite(p) ? p : PRIORITY_UNSET;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* 납기 대비 상태 — **파생**이다(저장하지 않는다). `locationStatusOf` 와 같은 규율.
|
|
198
|
+
*
|
|
199
|
+
* 예정 창(`endTime`)이 없으면 `undefined` — **"늦지 않았다" 가 아니라 "판단할 수 없다"** 다.
|
|
200
|
+
* 납기가 없는데 정시라고 말하면 그건 없는 사실을 만드는 것이다.
|
|
201
|
+
*/
|
|
202
|
+
export function dueStatusOf(x, nowIso) {
|
|
203
|
+
if (!x.endTime || !nowIso)
|
|
204
|
+
return undefined;
|
|
205
|
+
const due = Date.parse(x.endTime);
|
|
206
|
+
const now = Date.parse(nowIso);
|
|
207
|
+
if (!Number.isFinite(due) || !Number.isFinite(now))
|
|
208
|
+
return undefined;
|
|
209
|
+
return now > due ? 'late' : 'on-time';
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* 선언된 단위로 수량을 읽는다 — **환산하지 않는다.**
|
|
213
|
+
*
|
|
214
|
+
* 없으면 `undefined`: "0" 이 아니고 "계산한 값" 도 아니다. 환산 계수를 모르는데 값을 만들면
|
|
215
|
+
* 그 뒤 모든 계산이 거짓 위에 선다.
|
|
216
|
+
*/
|
|
217
|
+
/**
|
|
218
|
+
* 로트의 부분 식별자를 **한 규칙으로** 만든다 — 표준 `MaterialSubLot.ID`.
|
|
219
|
+
*
|
|
220
|
+
* 시뮬(생산)과 미러(관측)가 각자 만들면 같은 부분이 다른 이름을 갖고, 두 구동이 갈라진다
|
|
221
|
+
* (적합성 하네스가 실제로 잡았다). 부분을 가르는 것은 **자리**다.
|
|
222
|
+
*/
|
|
223
|
+
/**
|
|
224
|
+
* 물품을 구별하는 키 — 직렬 물품은 `epc`, 로트의 부분은 `subLotId`(표준 `MaterialSubLot.ID`).
|
|
225
|
+
*
|
|
226
|
+
* **한 곳에서 정한다.** 소비처마다 `epc` 로 키를 잡으면 같은 로트의 두 부분이 하나로 접히고,
|
|
227
|
+
* 그 순간 재고가 조용히 줄어든다(실제로 그랬다 — §ItemState.subLotId).
|
|
228
|
+
*/
|
|
229
|
+
export function subLotIdOf(classUri, location) {
|
|
230
|
+
return `${classUri}@${location}`;
|
|
231
|
+
}
|
|
232
|
+
export function itemKeyOf(item) {
|
|
233
|
+
return item.subLotId ?? item.epc;
|
|
234
|
+
}
|
|
235
|
+
export function quantityIn(item, uom, definitions) {
|
|
236
|
+
const hit = item.quantities?.find(q => (q.uom ?? undefined) === (uom ?? undefined));
|
|
237
|
+
if (hit)
|
|
238
|
+
return hit.value;
|
|
239
|
+
/* 주 수량이 그 단위면 그것을 답한다 — `quantities` 를 안 실은 물품도 답이 나오게. */
|
|
240
|
+
if ((item.uom ?? undefined) === (uom ?? undefined))
|
|
241
|
+
return item.qty;
|
|
242
|
+
/* **선언된 계수가 있으면** 환산한다 — 여전히 지어내지는 않는다(§conversionFactorOf). */
|
|
243
|
+
return convertQuantity(item, uom, definitions);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* **우리가 정한 품목 속성 이름** — 표준은 자리만 정하고 이름을 정하지 않는다.
|
|
247
|
+
*
|
|
248
|
+
* `perBaseUnit`: 값은 **기준 단위 하나당 그 단위의 양**이고, 단위는 속성의 `uom` 이 말한다.
|
|
249
|
+
* 예) 한 개(EA)가 2.5 kg 이면 `{ id: 'perBaseUnit', value: 2.5, uom: 'KGM' }`.
|
|
250
|
+
*
|
|
251
|
+
* **왜 이 모양인가**: 임의의 단위쌍 환산표(CS↔KGM↔EA…)는 품목마다 다르고 연쇄가 필요해 금세
|
|
252
|
+
* 커진다. 현장에서 실제로 필요한 것은 **"세는 단위에서 다른 단위로"** 이므로, 기준 단위를 축으로
|
|
253
|
+
* 두면 선언 한 줄로 끝난다. 기준 단위가 아닌 수량에서 출발하는 환산은 **하지 않는다**(§convertQuantity).
|
|
254
|
+
*/
|
|
255
|
+
export const MATERIAL_PROPERTY = {
|
|
256
|
+
/** 기준 단위 하나당 이 단위의 양. `uom` 이 대상 단위. */
|
|
257
|
+
perBaseUnit: 'perBaseUnit'
|
|
258
|
+
};
|
|
259
|
+
/**
|
|
260
|
+
* 이 품목에서 `uom` 으로 가는 계수 — **선언된 것만.** 없으면 `undefined`(추정하지 않는다).
|
|
261
|
+
*/
|
|
262
|
+
export function conversionFactorOf(def, uom) {
|
|
263
|
+
if (!def || !uom)
|
|
264
|
+
return undefined;
|
|
265
|
+
for (const p of def.properties ?? []) {
|
|
266
|
+
if (p.id !== MATERIAL_PROPERTY.perBaseUnit || p.uom !== uom)
|
|
267
|
+
continue;
|
|
268
|
+
const n = typeof p.value === 'number' ? p.value : Number(p.value);
|
|
269
|
+
if (Number.isFinite(n) && n > 0)
|
|
270
|
+
return n;
|
|
271
|
+
}
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* 선언된 계수로 환산한다 — **기준 수량에서만 출발한다.**
|
|
276
|
+
*
|
|
277
|
+
* 기준 수량(`qty`)은 품목을 세는 단위다(EPCIS 는 단위 미지정이면 개수로 읽는다). 그것이 아닌 수량에서
|
|
278
|
+
* 출발하려면 그 단위→기준 단위 역계수가 또 필요한데, 그것을 짐작하면 오차가 곱으로 쌓인다.
|
|
279
|
+
* 그래서 **기준 수량이 없거나 이미 다른 단위로 선언돼 있으면 환산하지 않는다.**
|
|
280
|
+
*/
|
|
281
|
+
function convertQuantity(item, uom, definitions) {
|
|
282
|
+
if (!definitions || !uom)
|
|
283
|
+
return undefined;
|
|
284
|
+
if (typeof item.qty !== 'number' || !Number.isFinite(item.qty))
|
|
285
|
+
return undefined;
|
|
286
|
+
if (item.uom)
|
|
287
|
+
return undefined; // 기준 단위가 아닌 수량 — 역계수를 지어내지 않는다
|
|
288
|
+
const def = definitions.get(item.definitionId ?? item.gtin ?? '');
|
|
289
|
+
const factor = conversionFactorOf(def, uom);
|
|
290
|
+
return factor === undefined ? undefined : item.qty * factor;
|
|
291
|
+
}
|
|
292
|
+
/** `HH:MM` → 자정 이후 분. 형식이 아니면 `undefined`(짐작해 고치지 않는다). */
|
|
293
|
+
function minutesOfDay(hhmm) {
|
|
294
|
+
const m = /^(\d{1,2}):(\d{2})$/.exec(hhmm ?? '');
|
|
295
|
+
if (!m)
|
|
296
|
+
return undefined;
|
|
297
|
+
const h = Number(m[1]);
|
|
298
|
+
const mi = Number(m[2]);
|
|
299
|
+
if (h > 23 || mi > 59)
|
|
300
|
+
return undefined;
|
|
301
|
+
return h * 60 + mi;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* 이 시각이 근무 시간인가 — **비근무가 근무를 이긴다.**
|
|
305
|
+
*
|
|
306
|
+
* 표준은 구간과 종류를 정하지만 **겹칠 때 무엇이 이기는지는 정하지 않는다.** 그래서 우리가 정했다:
|
|
307
|
+
* 휴일·정비(비근무)는 교대(근무) 위에 얹힌다. 반대로 두면 휴일 선언이 무의미해진다.
|
|
308
|
+
*
|
|
309
|
+
* 캘린더가 비어 있으면 `true` — **24시간 가용이 기존 거동**이고, 선언하지 않은 것을 쉬는 것으로
|
|
310
|
+
* 읽으면 아무 일도 일어나지 않는 트윈이 된다.
|
|
311
|
+
*/
|
|
312
|
+
/** 절대 구간을 쓰는 항목인가 — 되풀이(`HH:MM`)와 판정 방법이 다르다. */
|
|
313
|
+
const isAbsolute = (e) => !!(e.startDateTime || e.finishDateTime);
|
|
314
|
+
/** **날짜를 알아야** 판정할 수 있는 항목인가 — 절대 구간이거나 요일이 걸린 것. */
|
|
315
|
+
const needsDate = (e) => isAbsolute(e) || !!e.daysOfWeek?.length;
|
|
316
|
+
/**
|
|
317
|
+
* 이 구간이 **선언된 요일에 시작한 것**인가 — 자정을 넘는 교대를 바르게 세기 위한 규칙.
|
|
318
|
+
*
|
|
319
|
+
* 22:00→06:00 교대에 월~금을 주면 뜻은 "월~금에 **시작**한다" 이다. 그래서 토요일 새벽 2시는
|
|
320
|
+
* **금요일에 시작한 교대**의 일부이고(포함), 월요일 새벽 2시는 일요일 밤에 서지 않은 교대이므로
|
|
321
|
+
* 제외된다. 순간의 요일로 세면 이 둘이 정확히 반대로 틀린다.
|
|
322
|
+
*/
|
|
323
|
+
function onDeclaredDay(e, minuteOfDay, weekday) {
|
|
324
|
+
if (!e.daysOfWeek?.length)
|
|
325
|
+
return true;
|
|
326
|
+
const from = minutesOfDay(e.fromTime);
|
|
327
|
+
const to = minutesOfDay(e.toTime);
|
|
328
|
+
const crosses = from !== undefined && to !== undefined && from > to;
|
|
329
|
+
/* 자정을 넘는 구간의 **뒷조각**(자정~종료)은 어제 시작한 것이다. */
|
|
330
|
+
const startedDay = crosses && minuteOfDay < to ? (weekday + 6) % 7 : weekday;
|
|
331
|
+
return e.daysOfWeek.includes(startedDay);
|
|
332
|
+
}
|
|
333
|
+
/** 선언된 기준의 요일(0 일요일 … 6 토요일). */
|
|
334
|
+
export function weekdayAt(ms, utcOffsetMinutes) {
|
|
335
|
+
return new Date(ms + (utcOffsetMinutes ?? 0) * 60_000).getUTCDay();
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* 절대 구간이 그 시각을 덮는가 — 한쪽만 있으면 그쪽만 본다(열린 구간).
|
|
339
|
+
* 깨진 시각으로는 판정하지 않는다(짐작해 고치지 않는다).
|
|
340
|
+
*/
|
|
341
|
+
function coversInstant(e, atMs) {
|
|
342
|
+
const from = e.startDateTime ? Date.parse(e.startDateTime) : NaN;
|
|
343
|
+
const to = e.finishDateTime ? Date.parse(e.finishDateTime) : NaN;
|
|
344
|
+
if (!Number.isFinite(from) && !Number.isFinite(to))
|
|
345
|
+
return false;
|
|
346
|
+
if (Number.isFinite(from) && atMs < from)
|
|
347
|
+
return false;
|
|
348
|
+
if (Number.isFinite(to) && atMs >= to)
|
|
349
|
+
return false;
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
/** 이 구간이 그 분을 덮는가 — `inWorkCalendar` 와 `activeShiftOf` 가 **같은 규칙**을 쓴다. */
|
|
353
|
+
function coversMinute(e, minuteOfDay) {
|
|
354
|
+
const from = minutesOfDay(e.fromTime);
|
|
355
|
+
const to = minutesOfDay(e.toTime);
|
|
356
|
+
if (from === undefined || to === undefined)
|
|
357
|
+
return false; // 깨진 선언으로 판정하지 않는다
|
|
358
|
+
/* 자정을 넘는 구간(22:00→06:00)은 두 조각으로 읽는다 — 날짜 오프셋이 그것을 명시할 때도 같다. */
|
|
359
|
+
return from <= to ? minuteOfDay >= from && minuteOfDay < to : minuteOfDay >= from || minuteOfDay < to;
|
|
360
|
+
}
|
|
361
|
+
export function inWorkCalendar(entries, minuteOfDay) {
|
|
362
|
+
/* 되풀이만 본다 — 절대 구간은 분(minute)만으로 판정할 수 없다(어느 날인지 모른다).
|
|
363
|
+
시각(ms)을 가진 소비처는 `inWorkCalendarAt` 을 쓴다. */
|
|
364
|
+
return judgeCalendar(entries, e => coversMinute(e, minuteOfDay), false);
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* 이 **시각**이 근무 시간인가 — 되풀이(`HH:MM`)와 **한 번뿐인 구간**(휴일·정비창)을 함께 본다.
|
|
368
|
+
*
|
|
369
|
+
* 절대 구간은 시각 기준이 필요 없다(ISO 시각에 이미 들어 있다). 되풀이는 **선언된 기준**으로 읽는다.
|
|
370
|
+
* 겹치면 규칙은 하나다 — **비근무가 근무를 이긴다**(휴일이 교대 위에 얹힌다).
|
|
371
|
+
*/
|
|
372
|
+
export function inWorkCalendarAt(entries, atMs, utcOffsetMinutes) {
|
|
373
|
+
const minute = minuteOfDayAt(atMs, utcOffsetMinutes);
|
|
374
|
+
const day = weekdayAt(atMs, utcOffsetMinutes);
|
|
375
|
+
const covers = (e) => {
|
|
376
|
+
if (isAbsolute(e))
|
|
377
|
+
return coversInstant(e, atMs);
|
|
378
|
+
if (!coversMinute(e, minute))
|
|
379
|
+
return false;
|
|
380
|
+
/* 요일이 걸려 있으면 **그 구간이 시작한 날**로 센다(자정을 넘는 교대). */
|
|
381
|
+
return onDeclaredDay(e, minute, day);
|
|
382
|
+
};
|
|
383
|
+
return judgeCalendar(entries, covers, true);
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* 근무/비근무 판정의 **한 규칙** — 덮는 방법만 다르고 우선순위는 같다.
|
|
387
|
+
*
|
|
388
|
+
* `withAbsolute` 가 false 면 절대 구간 항목은 **없는 셈 친다**(판정할 재료가 없다). 그것을 근무로도
|
|
389
|
+
* 비근무로도 세면 둘 다 거짓이 된다 — 모르면 판단하지 않는다.
|
|
390
|
+
*/
|
|
391
|
+
function judgeCalendar(entries, covers, withAbsolute) {
|
|
392
|
+
if (!entries?.length)
|
|
393
|
+
return true;
|
|
394
|
+
const usable = withAbsolute ? entries : entries.filter(e => !needsDate(e));
|
|
395
|
+
if (!usable.length)
|
|
396
|
+
return true;
|
|
397
|
+
const working = usable.filter(e => (e.entryType ?? 'working') === 'working');
|
|
398
|
+
const off = usable.filter(e => e.entryType === 'non-working');
|
|
399
|
+
if (off.some(covers))
|
|
400
|
+
return false; // 비근무가 이긴다
|
|
401
|
+
if (!working.length)
|
|
402
|
+
return true; // 비근무만 선언했다면 그 밖은 근무다
|
|
403
|
+
return working.some(covers);
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* 지금 어느 교대인가 — **근무 구간의 이름**(표준 `WorkCalendarEntryType.ID`).
|
|
407
|
+
*
|
|
408
|
+
* 비근무가 이기는 규칙은 여기서도 같다: 휴게·정비 중이면 **어느 교대도 아니다**(`undefined`).
|
|
409
|
+
* 이름 없는 구간은 이름을 지어내지 않는다 — 교대를 나눠 놓지 않은 현장에서 `'1'` 같은 값을
|
|
410
|
+
* 만들어 붙이면, 그 뒤 모든 교대별 집계가 없는 구분 위에 선다.
|
|
411
|
+
*
|
|
412
|
+
* 겹치는 근무 구간이 여럿이면 **먼저 선언된 것**을 답한다(선언 순서가 현장의 우선순위다).
|
|
413
|
+
*/
|
|
414
|
+
export function activeShiftOf(entries, minuteOfDay) {
|
|
415
|
+
if (!entries?.length)
|
|
416
|
+
return undefined;
|
|
417
|
+
if (!inWorkCalendar(entries, minuteOfDay))
|
|
418
|
+
return undefined; // 비근무 중 — 어느 교대도 아니다
|
|
419
|
+
for (const e of entries) {
|
|
420
|
+
if (needsDate(e) || (e.entryType ?? 'working') !== 'working' || !e.id)
|
|
421
|
+
continue;
|
|
422
|
+
if (coversMinute(e, minuteOfDay))
|
|
423
|
+
return e.id;
|
|
424
|
+
}
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* 이 **시각**에 어느 교대인가 — 휴일까지 반영한다.
|
|
429
|
+
*
|
|
430
|
+
* 휴일에 일어난 일은 **어느 교대에도 속하지 않는다**(그날 교대는 서지 않았다). 되풀이만 보는
|
|
431
|
+
* `activeShiftOf` 로는 그것을 알 수 없어, 휴일에 찍힌 기록이 평소 교대로 집계됐다.
|
|
432
|
+
*/
|
|
433
|
+
export function activeShiftAt(entries, atMs, utcOffsetMinutes) {
|
|
434
|
+
if (!entries?.length)
|
|
435
|
+
return undefined;
|
|
436
|
+
if (!inWorkCalendarAt(entries, atMs, utcOffsetMinutes))
|
|
437
|
+
return undefined;
|
|
438
|
+
const minute = minuteOfDayAt(atMs, utcOffsetMinutes);
|
|
439
|
+
const day = weekdayAt(atMs, utcOffsetMinutes);
|
|
440
|
+
for (const e of entries) {
|
|
441
|
+
if (isAbsolute(e) || (e.entryType ?? 'working') !== 'working' || !e.id)
|
|
442
|
+
continue;
|
|
443
|
+
if (!coversMinute(e, minute))
|
|
444
|
+
continue;
|
|
445
|
+
if (!onDeclaredDay(e, minute, day))
|
|
446
|
+
continue; // 그날 서지 않은 교대(시작한 날로 센다)
|
|
447
|
+
return e.id;
|
|
448
|
+
}
|
|
449
|
+
return undefined;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* 절대 시각(ms) → **선언된 기준의** 하루 중 분(0..1439).
|
|
453
|
+
*
|
|
454
|
+
* `HH:MM` 만으로는 "어느 기준의 06시" 인지 알 수 없다. 예전에 이것을 UTC 로 읽어 Rosarito(UTC−7)의
|
|
455
|
+
* 06시 교대가 **7시간 틀렸다.** 선언이 없으면 UTC 이고, 그 기본값을 숨기지 않고 밝힌다.
|
|
456
|
+
*/
|
|
457
|
+
export function minuteOfDayAt(ms, utcOffsetMinutes) {
|
|
458
|
+
const d = new Date(ms + (utcOffsetMinutes ?? 0) * 60_000);
|
|
459
|
+
return d.getUTCHours() * 60 + d.getUTCMinutes();
|
|
460
|
+
}
|
|
461
|
+
export function offCalendarReasonAt(r, ms, utcOffsetMinutes) {
|
|
462
|
+
if (!offCalendarAt(r, ms, utcOffsetMinutes))
|
|
463
|
+
return undefined;
|
|
464
|
+
const entries = r.workCalendar;
|
|
465
|
+
if (!entries?.length)
|
|
466
|
+
return 'off-hours'; // 옛 `window` 경로 — 구간 선언이 없다
|
|
467
|
+
const minute = minuteOfDayAt(ms, utcOffsetMinutes);
|
|
468
|
+
const day = weekdayAt(ms, utcOffsetMinutes);
|
|
469
|
+
for (const e of entries) {
|
|
470
|
+
if (e.entryType !== 'non-working')
|
|
471
|
+
continue;
|
|
472
|
+
const covered = isAbsolute(e) ? coversInstant(e, ms) : coversMinute(e, minute) && onDeclaredDay(e, minute, day);
|
|
473
|
+
if (covered)
|
|
474
|
+
return 'non-working';
|
|
475
|
+
}
|
|
476
|
+
return 'off-hours';
|
|
477
|
+
}
|
|
478
|
+
export function offCalendarAt(r, ms, utcOffsetMinutes) {
|
|
479
|
+
const minute = minuteOfDayAt(ms, utcOffsetMinutes);
|
|
480
|
+
if (r.workCalendar?.length)
|
|
481
|
+
return !inWorkCalendarAt(r.workCalendar, ms, utcOffsetMinutes);
|
|
482
|
+
const w = r.window;
|
|
483
|
+
if (!w)
|
|
484
|
+
return false;
|
|
485
|
+
const h = Math.floor(minute / 60);
|
|
486
|
+
return !(w.startHour <= w.endHour ? h >= w.startHour && h < w.endHour : h >= w.startHour || h < w.endHour);
|
|
21
487
|
}
|
|
22
488
|
// ── 운영 델타(비-EPCIS) — State 채널의 나머지 절반 ──────────────────────────
|
|
23
|
-
// EPCIS 이벤트는 재고/위치만 재구성 가능. tasks·
|
|
489
|
+
// EPCIS 이벤트는 재고/위치만 재구성 가능. tasks·equipment·orders 의 운영 상태는
|
|
24
490
|
// 이 델타로 미러한다. envelope.eventType = 'task.status' | 'equipment.status' | 'order.status'.
|
|
25
491
|
// (execution-model.md §5, roadmap 발견 gap: 운영 델타 이벤트화)
|
|
26
492
|
export const OP_EVENT = {
|
|
@@ -40,11 +506,57 @@ export const CMD = {
|
|
|
40
506
|
orderResume: 'order.resume',
|
|
41
507
|
orderRelease: 'order.release',
|
|
42
508
|
attentionAck: 'attention.ack', // 주목 신호 확인(OPC UA A&C acknowledge) — args:{id}
|
|
43
|
-
// Operable 코어 — 모든 operable 자원(
|
|
509
|
+
// Operable 코어 — 모든 operable 자원(설비·이동설비) 공통. args:{resourceId}. capability-keyed(무방언, equipment.* 아님).
|
|
44
510
|
resourceHold: 'resource.hold', // 계획 정지(정비/오프라인) — 배정 스킵
|
|
45
511
|
resourceResume: 'resource.resume', // 계획 정지 해제
|
|
46
512
|
resourceDown: 'resource.down', // 비계획 고장 주입 — args:{resourceId, durationMs?}
|
|
47
513
|
resourceRepair: 'resource.repair', // 즉시 수리
|
|
48
514
|
resourceResetMetrics: 'resource.reset-metrics', // OEE 계측 창 리셋
|
|
49
|
-
resourceAdd: 'resource.add' // 라이브 자원(
|
|
515
|
+
resourceAdd: 'resource.add' // 라이브 자원(설비) 추가 — args:{kind, homeLocation, count?}. 런타임 구조 변이(what-if 아닌 실제 act)
|
|
50
516
|
};
|
|
517
|
+
// ── 보드 청사진 바인딩 (최소) ──────────────────────────────────────────────
|
|
518
|
+
/**
|
|
519
|
+
* **저장된 보드를 읽는 단 하나의 입구.**
|
|
520
|
+
*
|
|
521
|
+
* `equipment` 로 개명하기 전에 저장된 보드는 `movers` 키를 갖고 있다(개명 시점 23개 인스턴스). (vocabulary-guard: allow — 읽기 호환 설명)
|
|
522
|
+
* 저장물을 다시 쓰지 않고 **읽을 때 흡수**한다 — 마이그레이션은 되돌리기 어렵고, 읽기 호환은 값싸다.
|
|
523
|
+
*
|
|
524
|
+
* 규율 둘:
|
|
525
|
+
* - 이 함수를 **거치지 않고** `def.equipment` 를 직접 읽는 코드를 두지 않는다. 하나라도 남으면
|
|
526
|
+
* 그 경로에서만 옛 보드의 설비가 조용히 사라진다(빈 배열).
|
|
527
|
+
* - **쓸 때는 새 이름만** 쓴다. 두 이름으로 쓰기 시작하면 저장물에 두 벌이 영구히 섞인다.
|
|
528
|
+
*
|
|
529
|
+
* 제거 시점: 저장된 보드가 모두 `equipment` 키로 바뀐 것이 확인되면(운영 데이터 점검 후) 이 함수는
|
|
530
|
+
* 사라진다. 그때까지 남겨 두는 이유를 여기 적어 두는 것이 주석의 일이다.
|
|
531
|
+
*/
|
|
532
|
+
/**
|
|
533
|
+
* **저장된 보드의 자리를 읽는 단 하나의 입구.** `readBoardEquipment` 와 같은 규율.
|
|
534
|
+
*
|
|
535
|
+
* `nodes` → `locations` 개명(2026-08-01) 전에 저장된 보드는 `nodes` 키를 갖고 있다(개명 시점 23개). (vocabulary-guard: allow — 읽기 호환 설명)
|
|
536
|
+
* 이 함수를 거치지 않고 `def.locations` 를 직접 읽는 코드를 두지 않는다 — 하나라도 남으면 그 경로에서만
|
|
537
|
+
* 옛 보드의 자리가 조용히 사라진다(빈 배열 = 자리 없는 트윈 = 아무 일도 일어나지 않는다).
|
|
538
|
+
*/
|
|
539
|
+
export function readBoardLocations(def) {
|
|
540
|
+
const d = def; // vocabulary-guard: allow — 옛 키 캐스트
|
|
541
|
+
return d.locations ?? d.nodes ?? []; // vocabulary-guard: allow — 옛 키 흡수
|
|
542
|
+
}
|
|
543
|
+
export function readBoardEquipment(def) {
|
|
544
|
+
const d = def; // vocabulary-guard: allow — 옛 키를 읽어야 하는 자리
|
|
545
|
+
const list = d.equipment ?? d.equipmentList ?? []; // vocabulary-guard: allow — 옛 키 흡수
|
|
546
|
+
/* 소속 자리 키도 함께 정규화한다 — `homeNode → homeLocation` 개명 전 보드가 23개 있다. (vocabulary-guard: allow — 옛 키 정규화 설명)
|
|
547
|
+
배열만 흡수하고 안쪽 키를 놓치면 설비는 나타나지만 **소속이 전부 비어** 롤업이 통째로 사라진다. */
|
|
548
|
+
return list.map(e => normalizeHomeLocation(e));
|
|
549
|
+
}
|
|
550
|
+
/** 소속 자리 키 정규화 — 설비·자산이 같은 규칙을 쓴다(둘 다 옛 이름이 저장돼 있다). */
|
|
551
|
+
function normalizeHomeLocation(entry) {
|
|
552
|
+
const legacy = entry.homeNode; // vocabulary-guard: allow — 옛 키를 읽어야 하는 자리
|
|
553
|
+
if (legacy === undefined || entry.homeLocation !== undefined)
|
|
554
|
+
return entry;
|
|
555
|
+
const { homeNode: _drop, ...rest } = entry; // vocabulary-guard: allow — 옛 키 제거
|
|
556
|
+
return { ...rest, homeLocation: legacy };
|
|
557
|
+
}
|
|
558
|
+
/** 저장된 보드의 반복사용 자산 — 설비와 같은 정규화를 거친다. */
|
|
559
|
+
export function readBoardAssets(def) {
|
|
560
|
+
const list = (def.assets ?? []);
|
|
561
|
+
return list.map(a => normalizeHomeLocation(a));
|
|
562
|
+
}
|
package/dist/divergence.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export interface StateDivergence {
|
|
|
8
8
|
hasDrift: boolean;
|
|
9
9
|
itemLocation: FieldDiff<string>[];
|
|
10
10
|
itemDisposition: FieldDiff<string>[];
|
|
11
|
-
|
|
11
|
+
locationOccupancy: FieldDiff<number>[];
|
|
12
12
|
orderStatus: FieldDiff<string>[];
|
|
13
13
|
}
|
|
14
14
|
/** predicted(fork forecast)와 actual(관측) 스냅샷을 대조. 같은 sim 시점에 호출하는 것이 의미 있음. */
|
package/dist/divergence.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* 상태를 같은 시점에 비교해 **드리프트(모델과 현실의 이탈)** 를 탐지한다. 발산 = 이상/개입 신호.
|
|
6
6
|
* (fork 로 예측 → 관측으로 실제 → 여기서 대조. execution-model.md 시뮬↔모니터링 커플링.)
|
|
7
7
|
*/
|
|
8
|
+
import { itemKeyOf } from "./contract.js";
|
|
8
9
|
function diffBy(predicted, actual, idOf, valOf) {
|
|
9
10
|
const p = new Map(predicted.map(e => [idOf(e), valOf(e)]));
|
|
10
11
|
const a = new Map(actual.map(e => [idOf(e), valOf(e)]));
|
|
@@ -19,10 +20,12 @@ function diffBy(predicted, actual, idOf, valOf) {
|
|
|
19
20
|
}
|
|
20
21
|
/** predicted(fork forecast)와 actual(관측) 스냅샷을 대조. 같은 sim 시점에 호출하는 것이 의미 있음. */
|
|
21
22
|
export function compareStates(predicted, actual) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
23
|
+
/* 키는 **한 규칙**으로 잡는다(`itemKeyOf`) — 같은 로트의 두 부분을 `epc` 로 접으면 발산 비교가
|
|
24
|
+
"같은 물품이 두 곳에 있다" 를 자기 오류로 착각한다. */
|
|
25
|
+
const itemLocation = diffBy(predicted.items, actual.items, itemKeyOf, i => i.location);
|
|
26
|
+
const itemDisposition = diffBy(predicted.items, actual.items, itemKeyOf, i => i.disposition);
|
|
27
|
+
const locationOccupancy = diffBy(predicted.locations, actual.locations, n => n.id, n => n.occupancy);
|
|
25
28
|
const orderStatus = diffBy(predicted.orders, actual.orders, o => o.id, o => o.status);
|
|
26
|
-
const hasDrift = itemLocation.length > 0 || itemDisposition.length > 0 ||
|
|
27
|
-
return { hasDrift, itemLocation, itemDisposition,
|
|
29
|
+
const hasDrift = itemLocation.length > 0 || itemDisposition.length > 0 || locationOccupancy.length > 0 || orderStatus.length > 0;
|
|
30
|
+
return { hasDrift, itemLocation, itemDisposition, locationOccupancy, orderStatus };
|
|
28
31
|
}
|
package/dist/domain-catalog.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export interface TwinTypeInfo {
|
|
|
4
4
|
/** 커널 권위 키 (예: 'storage', 'forklift'). */
|
|
5
5
|
key: string;
|
|
6
6
|
/** 로케이션(수동) vs 자원(능동). */
|
|
7
|
-
role: '
|
|
7
|
+
role: 'location' | 'equipment';
|
|
8
8
|
label: string;
|
|
9
9
|
/** ① 표준 온톨로지 투영(03-reference-standards). 열린 문자열 — 하드코딩 enum 금지. */
|
|
10
10
|
standardClass: {
|
|
@@ -22,10 +22,10 @@ export interface TwinTypeInfo {
|
|
|
22
22
|
export interface DomainProfileInfo {
|
|
23
23
|
system: DomainSystem;
|
|
24
24
|
label: string;
|
|
25
|
-
/**
|
|
25
|
+
/** 자리+설비 트윈 타입 — board·UI 팔레트 소싱 SSOT. */
|
|
26
26
|
types: TwinTypeInfo[];
|
|
27
|
-
/** 로케이션
|
|
28
|
-
|
|
27
|
+
/** 로케이션 자리 타입 키(하위호환 파생 뷰 = types 중 role==='location'). 신규 소싱은 types 사용. */
|
|
28
|
+
locationTypes: readonly string[];
|
|
29
29
|
}
|
|
30
30
|
export declare const DOMAIN_CATALOG: Record<DomainSystem, DomainProfileInfo>;
|
|
31
31
|
export declare const DOMAIN_SYSTEMS: DomainSystem[];
|
package/dist/domain-catalog.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { WMS_TYPES } from "./wms-profile.js";
|
|
2
2
|
import { YMS_TYPES } from "./yms-profile.js";
|
|
3
3
|
import { MES_TYPES } from "./mes-profile.js";
|
|
4
|
-
const
|
|
4
|
+
const locationKeys = (types) => types.filter(t => t.role === 'location').map(t => t.key);
|
|
5
5
|
export const DOMAIN_CATALOG = {
|
|
6
6
|
// label 은 언어 중립 i18n 키(twin.system.<code>) — 사람 언어는 표현계층이 렌더(L2).
|
|
7
|
-
wms: { system: 'wms', label: 'twin.system.wms', types: WMS_TYPES,
|
|
8
|
-
yms: { system: 'yms', label: 'twin.system.yms', types: YMS_TYPES,
|
|
9
|
-
mes: { system: 'mes', label: 'twin.system.mes', types: MES_TYPES,
|
|
7
|
+
wms: { system: 'wms', label: 'twin.system.wms', types: WMS_TYPES, locationTypes: locationKeys(WMS_TYPES) },
|
|
8
|
+
yms: { system: 'yms', label: 'twin.system.yms', types: YMS_TYPES, locationTypes: locationKeys(YMS_TYPES) },
|
|
9
|
+
mes: { system: 'mes', label: 'twin.system.mes', types: MES_TYPES, locationTypes: locationKeys(MES_TYPES) }
|
|
10
10
|
};
|
|
11
11
|
export const DOMAIN_SYSTEMS = ['wms', 'yms', 'mes'];
|
|
12
12
|
/** 타입 키 → 능력 프로파일(커널 SSOT). 호스트가 라이브 페이로드에 투영, 컴포넌트가 능력을 렌더. */
|