@operato/ops-contract 0.9.4 → 0.9.6
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/dist/contract.d.ts +76 -0
- package/dist/contract.js +45 -0
- package/dist/domain-catalog.d.ts +39 -0
- package/dist/domain-catalog.js +46 -34
- package/dist/reliability.d.ts +35 -0
- package/dist/reliability.js +64 -15
- package/dist-cjs/index.cjs +91 -48
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -1477,6 +1477,28 @@ export type OeeMissing =
|
|
|
1477
1477
|
| 'produced-quantity'
|
|
1478
1478
|
/** 실 생산 시간이 0 이다 — 성능률의 분모가 없다. */
|
|
1479
1479
|
| 'actual-production-time';
|
|
1480
|
+
/**
|
|
1481
|
+
* 설비의 소속이 어느 단인가 — 자리(작업 셀)인가 구역(라인·구역)인가.
|
|
1482
|
+
*
|
|
1483
|
+
* ISA-95 의 설비 계층에서 설비는 어느 단에도 있을 수 있다. 라인에 딸린 부대 설비(배기·집진·세척·
|
|
1484
|
+
* 출입 설비)가 그 경우다 — 어느 작업 셀에도 안 붙는다.
|
|
1485
|
+
*/
|
|
1486
|
+
export type HomeKind = 'location' | 'area';
|
|
1487
|
+
/**
|
|
1488
|
+
* 그 설비의 소속과 단 — **없으면 자리다.**
|
|
1489
|
+
*
|
|
1490
|
+
* 소비처가 저마다 `?? 'location'` 을 적지 않게 한 자리에 둔다. 이 기본값은 짐작이 아니다: 이 칸이
|
|
1491
|
+
* 생기기 전의 모든 값은 자리였고(커널이 그것만 받았다) 필드 이름 자체가 `homeLocation` 이다.
|
|
1492
|
+
*
|
|
1493
|
+
* 소속 자체가 없으면 답하지 않는다 — 단만 있고 대상이 없는 것은 뜻이 없다.
|
|
1494
|
+
*/
|
|
1495
|
+
export declare function homeOf(e: {
|
|
1496
|
+
homeLocation?: string;
|
|
1497
|
+
homeLocationKind?: HomeKind;
|
|
1498
|
+
} | undefined): {
|
|
1499
|
+
id: string;
|
|
1500
|
+
kind: HomeKind;
|
|
1501
|
+
} | undefined;
|
|
1480
1502
|
export interface EquipmentState extends EffectivePeriod {
|
|
1481
1503
|
id: string;
|
|
1482
1504
|
kind: string;
|
|
@@ -1504,6 +1526,28 @@ export interface EquipmentState extends EffectivePeriod {
|
|
|
1504
1526
|
* 그래서 이동 설비가 한 번 움직이면 원래 소속을 아무도 알 수 없었다.
|
|
1505
1527
|
*/
|
|
1506
1528
|
homeLocation?: string;
|
|
1529
|
+
/**
|
|
1530
|
+
* 그 소속이 **어느 단인가** — 자리(작업 셀)인가 구역(라인·구역)인가.
|
|
1531
|
+
*
|
|
1532
|
+
* ── 왜 열었나 (2026-09-05 실측) ────────────────────────────────────────────
|
|
1533
|
+
* 미라텍 라인에 에어 샤워가 있다. 청정실 출입구이고 **제품에 손대지 않는다** — 등급도
|
|
1534
|
+
* `EC-CAM-FACILITY`(부대 설비)다. 그 라인의 셋은 다 공정 셀인데 에어 샤워는 그 셀들에 들어가기
|
|
1535
|
+
* **전** 출입구라 어느 셀에도 안 붙는다.
|
|
1536
|
+
*
|
|
1537
|
+
* 원본이 그것을 선언한다 — `homeLocationType: production-line`. 그런데 커널이 자리만 받아서 그
|
|
1538
|
+
* 설비 하나가 「이 트윈에 없는 것을 가리킨다」로 남았다.
|
|
1539
|
+
*
|
|
1540
|
+
* ISA-95 로도 맞다: 설비 계층은 Site › Area › ProductionLine › WorkCell 이고 **설비는 어느 단에도
|
|
1541
|
+
* 있을 수 있다.** 라인 배기·집진·세척처럼 라인에 딸린 설비는 흔한 모양이다.
|
|
1542
|
+
*
|
|
1543
|
+
* ── 셀을 지어 붙이지 않는다 ────────────────────────────────────────────────
|
|
1544
|
+
* 「청정실 출입」 셀을 만들어 거기 붙이면 그 라인이 **네 공정으로 보인다.** 실제는 셋이다. 원본이
|
|
1545
|
+
* 그러지 말라고 명시했고, 지어낸 자리는 화면이 실제 배치로 읽는다.
|
|
1546
|
+
*
|
|
1547
|
+
* 이 필드의 주석이 이미 「이 라인의 설비 가동률」이라고 적고 있었다 — 뜻은 처음부터 소속이었고
|
|
1548
|
+
* 구현만 자리를 요구했다.
|
|
1549
|
+
*/
|
|
1550
|
+
homeLocationKind?: HomeKind;
|
|
1507
1551
|
status: string;
|
|
1508
1552
|
taskId?: string;
|
|
1509
1553
|
motion?: EquipmentMotion;
|
|
@@ -3172,6 +3216,38 @@ export declare const EQUIPMENT_STATUS: readonly ["busy", "setup", "down", "idle"
|
|
|
3172
3216
|
export type EquipmentStatus = (typeof EQUIPMENT_STATUS)[number];
|
|
3173
3217
|
/** 선언된 상태인가. */
|
|
3174
3218
|
export declare function isEquipmentStatus(value: unknown): value is EquipmentStatus;
|
|
3219
|
+
/**
|
|
3220
|
+
* **ISA-95 의 네 자원** — 「무엇이 자원인가」를 한 자리에서 말한다.
|
|
3221
|
+
*
|
|
3222
|
+
* ── 왜 필요한가 (MES 레인이 물음, 2026-09-05) ──────────────────────────────
|
|
3223
|
+
* 계약이 네 자원을 여러 곳에서 다루는데, **넷이라고 말하는 값이 없었다.** 그래서 소비처가 저마다
|
|
3224
|
+
* 손으로 든다 — 자원 하나에 걸린 것을 한자리에 보이는 화면(360)이 종류마다 무엇을 물을지 정하려면
|
|
3225
|
+
* 그 목록이 필요하고, 지금은 화면마다 적게 된다.
|
|
3226
|
+
*
|
|
3227
|
+
* 이미 갈라지기 시작했다: `CapacityAxis`(§`capacity.ts`)가 넷을 손으로 들고 있다.
|
|
3228
|
+
* `EQUIPMENT_STATUS` 가 정확히 같은 자리였다 — 계약에 있는데 소비처가 따로 선언하고 있었다.
|
|
3229
|
+
*
|
|
3230
|
+
* ── 왜 넷만인가 ────────────────────────────────────────────────────────────
|
|
3231
|
+
* 표준이 넷이라고 말한다(ISA-95 Part 1 §5: Personnel · Equipment · Material · Physical Asset).
|
|
3232
|
+
* 자리·공정·로트를 여기 섞고 싶은 힘이 있는데 — 화면이 그것들도 보여 주므로 — **섞으면 이 상수가
|
|
3233
|
+
* ISA-95 를 말하지 않게 된다.** 그러면 다음 사람이 이것을 근거로 쓸 수 없다.
|
|
3234
|
+
*
|
|
3235
|
+
* 자리 자원을 **담는 곳**이다. 자원이 아니다
|
|
3236
|
+
* 로트 자재의 **개체**다. 자재 종류와 다른 층이다
|
|
3237
|
+
* 공정 자원을 **쓰는 일**이다
|
|
3238
|
+
*
|
|
3239
|
+
* 화면이 그 셋도 필요하면 다른 이름으로 든다. 이 목록은 「무엇이 자원인가」에만 답한다.
|
|
3240
|
+
*
|
|
3241
|
+
* ── `CapacityAxis` 와 다르다 ───────────────────────────────────────────────
|
|
3242
|
+
* 그것은 **제약이 걸린 축**이고(무엇을 늘려야 하는가), 자리가 들어간다 — 자리가 모자라도 못 돌기
|
|
3243
|
+
* 때문이다. 자재는 없다(늘린다고 지금 나오는 것이 아니다). 둘은 겹치지만 같지 않다.
|
|
3244
|
+
*
|
|
3245
|
+
* 합치면 한 값이 두 물음에 답하게 되고, 그러면 어느 쪽 뜻으로 읽어야 하는지 알 수 없다.
|
|
3246
|
+
*/
|
|
3247
|
+
export declare const RESOURCE_KIND: readonly ["equipment", "personnel", "material", "asset"];
|
|
3248
|
+
export type ResourceKind = (typeof RESOURCE_KIND)[number];
|
|
3249
|
+
/** 선언된 자원 종류인가. */
|
|
3250
|
+
export declare function isResourceKind(value: unknown): value is ResourceKind;
|
|
3175
3251
|
/**
|
|
3176
3252
|
* 원본의 낱말을 우리 상태로 옮긴다 — **모르면 `undefined` 다.**
|
|
3177
3253
|
*
|
package/dist/contract.js
CHANGED
|
@@ -1013,6 +1013,18 @@ export function observationAt(observations, propertyId, at) {
|
|
|
1013
1013
|
}
|
|
1014
1014
|
return best;
|
|
1015
1015
|
}
|
|
1016
|
+
/**
|
|
1017
|
+
* 그 설비의 소속과 단 — **없으면 자리다.**
|
|
1018
|
+
*
|
|
1019
|
+
* 소비처가 저마다 `?? 'location'` 을 적지 않게 한 자리에 둔다. 이 기본값은 짐작이 아니다: 이 칸이
|
|
1020
|
+
* 생기기 전의 모든 값은 자리였고(커널이 그것만 받았다) 필드 이름 자체가 `homeLocation` 이다.
|
|
1021
|
+
*
|
|
1022
|
+
* 소속 자체가 없으면 답하지 않는다 — 단만 있고 대상이 없는 것은 뜻이 없다.
|
|
1023
|
+
*/
|
|
1024
|
+
export function homeOf(e) {
|
|
1025
|
+
const id = e?.homeLocation;
|
|
1026
|
+
return id ? { id, kind: e?.homeLocationKind ?? 'location' } : undefined;
|
|
1027
|
+
}
|
|
1016
1028
|
// ── 운영 델타(비-EPCIS) — State 채널의 나머지 절반 ──────────────────────────
|
|
1017
1029
|
// EPCIS 이벤트는 재고/위치만 재구성 가능. tasks·equipment·orders 의 운영 상태는
|
|
1018
1030
|
// 이 델타로 미러한다. envelope.eventType = 'task.status' | 'equipment.status' | 'order.status'.
|
|
@@ -1276,6 +1288,39 @@ export const EQUIPMENT_STATUS = ['busy', 'setup', 'down', 'idle', 'planned-stop'
|
|
|
1276
1288
|
export function isEquipmentStatus(value) {
|
|
1277
1289
|
return typeof value === 'string' && EQUIPMENT_STATUS.includes(value);
|
|
1278
1290
|
}
|
|
1291
|
+
/**
|
|
1292
|
+
* **ISA-95 의 네 자원** — 「무엇이 자원인가」를 한 자리에서 말한다.
|
|
1293
|
+
*
|
|
1294
|
+
* ── 왜 필요한가 (MES 레인이 물음, 2026-09-05) ──────────────────────────────
|
|
1295
|
+
* 계약이 네 자원을 여러 곳에서 다루는데, **넷이라고 말하는 값이 없었다.** 그래서 소비처가 저마다
|
|
1296
|
+
* 손으로 든다 — 자원 하나에 걸린 것을 한자리에 보이는 화면(360)이 종류마다 무엇을 물을지 정하려면
|
|
1297
|
+
* 그 목록이 필요하고, 지금은 화면마다 적게 된다.
|
|
1298
|
+
*
|
|
1299
|
+
* 이미 갈라지기 시작했다: `CapacityAxis`(§`capacity.ts`)가 넷을 손으로 들고 있다.
|
|
1300
|
+
* `EQUIPMENT_STATUS` 가 정확히 같은 자리였다 — 계약에 있는데 소비처가 따로 선언하고 있었다.
|
|
1301
|
+
*
|
|
1302
|
+
* ── 왜 넷만인가 ────────────────────────────────────────────────────────────
|
|
1303
|
+
* 표준이 넷이라고 말한다(ISA-95 Part 1 §5: Personnel · Equipment · Material · Physical Asset).
|
|
1304
|
+
* 자리·공정·로트를 여기 섞고 싶은 힘이 있는데 — 화면이 그것들도 보여 주므로 — **섞으면 이 상수가
|
|
1305
|
+
* ISA-95 를 말하지 않게 된다.** 그러면 다음 사람이 이것을 근거로 쓸 수 없다.
|
|
1306
|
+
*
|
|
1307
|
+
* 자리 자원을 **담는 곳**이다. 자원이 아니다
|
|
1308
|
+
* 로트 자재의 **개체**다. 자재 종류와 다른 층이다
|
|
1309
|
+
* 공정 자원을 **쓰는 일**이다
|
|
1310
|
+
*
|
|
1311
|
+
* 화면이 그 셋도 필요하면 다른 이름으로 든다. 이 목록은 「무엇이 자원인가」에만 답한다.
|
|
1312
|
+
*
|
|
1313
|
+
* ── `CapacityAxis` 와 다르다 ───────────────────────────────────────────────
|
|
1314
|
+
* 그것은 **제약이 걸린 축**이고(무엇을 늘려야 하는가), 자리가 들어간다 — 자리가 모자라도 못 돌기
|
|
1315
|
+
* 때문이다. 자재는 없다(늘린다고 지금 나오는 것이 아니다). 둘은 겹치지만 같지 않다.
|
|
1316
|
+
*
|
|
1317
|
+
* 합치면 한 값이 두 물음에 답하게 되고, 그러면 어느 쪽 뜻으로 읽어야 하는지 알 수 없다.
|
|
1318
|
+
*/
|
|
1319
|
+
export const RESOURCE_KIND = ['equipment', 'personnel', 'material', 'asset'];
|
|
1320
|
+
/** 선언된 자원 종류인가. */
|
|
1321
|
+
export function isResourceKind(value) {
|
|
1322
|
+
return typeof value === 'string' && RESOURCE_KIND.includes(value);
|
|
1323
|
+
}
|
|
1279
1324
|
/**
|
|
1280
1325
|
* 원본의 낱말을 우리 상태로 옮긴다 — **모르면 `undefined` 다.**
|
|
1281
1326
|
*
|
package/dist/domain-catalog.d.ts
CHANGED
|
@@ -170,6 +170,25 @@ export type TwinRelationTarget = {
|
|
|
170
170
|
kind: 'external';
|
|
171
171
|
entity: string;
|
|
172
172
|
};
|
|
173
|
+
/**
|
|
174
|
+
* **그 관계가 가리키는 것이 언제의 일인가** — 화면의 판단이 여기서 갈린다.
|
|
175
|
+
*
|
|
176
|
+
* ── 왜 필요한가 (MES 레인이 실측으로 올림, 2026-09-05) ─────────────────────
|
|
177
|
+
* 설비 하나에 걸린 것을 한자리에 보이면, 들어오는 관계라도 뜻이 갈린다.
|
|
178
|
+
*
|
|
179
|
+
* past 이미 일어났다 — 세워도 안 바뀐다 실적 · 상태 구간
|
|
180
|
+
* future 아직이다 — 세우면 이것이 걸린다 정비 계획 · 지시
|
|
181
|
+
* standing 시간이 없는 사실 등급 · 붙박인 자리 · 종류
|
|
182
|
+
*
|
|
183
|
+
* 한 묶음에 담으면 **「걸린 것이 열」인데 아홉이 지난 일인 경우와 아홉이 앞의 일인 경우가 같아
|
|
184
|
+
* 보인다.** 「이 설비를 세워도 되나」의 답이 정반대인 두 상황이다.
|
|
185
|
+
*
|
|
186
|
+
* ── 왜 `via` 로 가르면 안 되나 ──────────────────────────────────────────────
|
|
187
|
+
* `via` 는 i18n 키다. **「이것이 지난 일인가」를 말하지 않는다.** 화면이 키 이름을 보고 짐작하게
|
|
188
|
+
* 되고, 축이 하나 늘 때 한 화면만 따라간다. 읽는 곳은 제품마다여도 **무엇을 아는지는 한 벌**이어야
|
|
189
|
+
* 한다.
|
|
190
|
+
*/
|
|
191
|
+
export type RelationTense = 'past' | 'future' | 'standing';
|
|
173
192
|
export interface TwinRelationInfo {
|
|
174
193
|
/** 출발 축. */
|
|
175
194
|
from: string;
|
|
@@ -183,7 +202,27 @@ export interface TwinRelationInfo {
|
|
|
183
202
|
via: string;
|
|
184
203
|
/** 없을 수 있는 참조인가. `false` 인데 비면 **끊어진 참조**로 보고한다. */
|
|
185
204
|
optional?: boolean;
|
|
205
|
+
/**
|
|
206
|
+
* 언제의 일인가. **없으면 「선언이 말하지 않았다」** — `standing` 이 아니다.
|
|
207
|
+
*
|
|
208
|
+
* 기본값을 두고 싶은 힘이 있는데 두면 거짓이 된다. 예를 들어 `tasks.resourceRef → equipment` 는
|
|
209
|
+
* **그 작업이 끝났는지에 따라** 과거이기도 미래이기도 하다. 선언이 답할 수 없는 자리이고, 거기
|
|
210
|
+
* `standing` 을 씌우면 「시간이 없는 사실」이라고 잘못 말하게 된다.
|
|
211
|
+
*
|
|
212
|
+
* 그러니 말할 수 있는 관계만 말한다. 안 말한 것을 화면이 시간으로 묶으려 하면 안 된다.
|
|
213
|
+
*/
|
|
214
|
+
tense?: RelationTense;
|
|
186
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* 그 관계가 언제의 일인가 — **모르면 `undefined`.**
|
|
218
|
+
*
|
|
219
|
+
* 소비처가 저마다 `?? 'standing'` 을 적지 않게 한 자리에 둔다. 그리고 그 기본값을 **주지 않는
|
|
220
|
+
* 것**이 이 함수의 요점이다 — 선언이 말하지 않은 것을 「시간이 없는 사실」로 바꾸면 화면이 모르는
|
|
221
|
+
* 것을 아는 척한다.
|
|
222
|
+
*
|
|
223
|
+
* 시간으로 묶는 화면은 `undefined` 인 것을 **묶지 않고 그대로 둔다.**
|
|
224
|
+
*/
|
|
225
|
+
export declare function tenseOf(r: Pick<TwinRelationInfo, 'tense'> | undefined): RelationTense | undefined;
|
|
187
226
|
/** 관계 전체 — 지도의 선과 항목의 이웃이 여기서 나온다(화면은 이 목록을 갖지 않는다). */
|
|
188
227
|
export declare const TWIN_RELATIONS: TwinRelationInfo[];
|
|
189
228
|
export type TwinObservationKind =
|
package/dist/domain-catalog.js
CHANGED
|
@@ -149,24 +149,36 @@ export const TWIN_AXES = [
|
|
|
149
149
|
{ axis: 'demandWindows', path: 'energy.closed', idField: 'startMs', label: 'twin.axis.demandWindows', kind: 'instance',
|
|
150
150
|
source: 'state', historical: true, standardClass: {}, systems: ['ems'] }
|
|
151
151
|
];
|
|
152
|
+
/**
|
|
153
|
+
* 그 관계가 언제의 일인가 — **모르면 `undefined`.**
|
|
154
|
+
*
|
|
155
|
+
* 소비처가 저마다 `?? 'standing'` 을 적지 않게 한 자리에 둔다. 그리고 그 기본값을 **주지 않는
|
|
156
|
+
* 것**이 이 함수의 요점이다 — 선언이 말하지 않은 것을 「시간이 없는 사실」로 바꾸면 화면이 모르는
|
|
157
|
+
* 것을 아는 척한다.
|
|
158
|
+
*
|
|
159
|
+
* 시간으로 묶는 화면은 `undefined` 인 것을 **묶지 않고 그대로 둔다.**
|
|
160
|
+
*/
|
|
161
|
+
export function tenseOf(r) {
|
|
162
|
+
return r?.tense;
|
|
163
|
+
}
|
|
152
164
|
/** 관계 전체 — 지도의 선과 항목의 이웃이 여기서 나온다(화면은 이 목록을 갖지 않는다). */
|
|
153
165
|
export const TWIN_RELATIONS = [
|
|
154
166
|
/* 자리가 속한 구역 — **board 밖**(호스트의 `TwinArea`). 해소는 호스트가 한다. */
|
|
155
|
-
{ from: 'locations', field: 'parentId', target: { kind: 'external', entity: 'space.area' }, via: '
|
|
167
|
+
{ from: 'locations', field: 'parentId', target: { kind: 'external', entity: 'space.area' }, via: 'ops.rel.area', optional: true },
|
|
156
168
|
/* 자원이 사는 자리 */
|
|
157
|
-
{ from: 'equipment', field: 'homeLocation', target: { kind: 'axis', axis: 'locations' }, via: '
|
|
158
|
-
{ from: 'persons', field: 'homeLocation', target: { kind: 'axis', axis: 'locations' }, via: '
|
|
159
|
-
{ from: 'assets', field: 'homeLocation', target: { kind: 'axis', axis: 'locations' }, via: '
|
|
169
|
+
{ from: 'equipment', field: 'homeLocation', target: { kind: 'axis', axis: 'locations' }, via: 'ops.rel.home', optional: true },
|
|
170
|
+
{ from: 'persons', field: 'homeLocation', target: { kind: 'axis', axis: 'locations' }, via: 'ops.rel.home', optional: true },
|
|
171
|
+
{ from: 'assets', field: 'homeLocation', target: { kind: 'axis', axis: 'locations' }, via: 'ops.rel.home', optional: true },
|
|
160
172
|
/* 자원의 등급 — 설비만 `kind`(타입)로 가고 사람·자산은 등급 목록으로 간다. 표준의 비대칭 그대로. */
|
|
161
|
-
{ from: 'persons', field: 'personnelClassIds[]', target: { kind: 'axis', axis: 'personnelClasses' }, via: '
|
|
162
|
-
{ from: 'assets', field: 'assetClassIds[]', target: { kind: 'axis', axis: 'assetClasses' }, via: '
|
|
163
|
-
{ from: 'equipment', field: 'kind', target: { kind: 'type', role: 'equipment' }, via: '
|
|
164
|
-
{ from: 'locations', field: 'type', target: { kind: 'type', role: 'location' }, via: '
|
|
173
|
+
{ from: 'persons', field: 'personnelClassIds[]', target: { kind: 'axis', axis: 'personnelClasses' }, via: 'ops.rel.class', optional: true },
|
|
174
|
+
{ from: 'assets', field: 'assetClassIds[]', target: { kind: 'axis', axis: 'assetClasses' }, via: 'ops.rel.class', optional: true },
|
|
175
|
+
{ from: 'equipment', field: 'kind', target: { kind: 'type', role: 'equipment' }, via: 'ops.rel.type' },
|
|
176
|
+
{ from: 'locations', field: 'type', target: { kind: 'type', role: 'location' }, via: 'ops.rel.type' },
|
|
165
177
|
/* 생산 — 레시피에서 시작해 라우트·공정·품목으로 퍼진다 */
|
|
166
|
-
{ from: 'recipes', field: 'route', target: { kind: 'axis', axis: 'routes' }, via: '
|
|
167
|
-
{ from: 'recipes', field: 'inputs[].material', target: { kind: 'axis', axis: 'materials' }, via: '
|
|
168
|
-
{ from: 'recipes', field: 'outputs[].material', target: { kind: 'axis', axis: 'materials' }, via: '
|
|
169
|
-
{ from: 'routes', field: 'steps[]', target: { kind: 'axis', axis: 'operations' }, via: '
|
|
178
|
+
{ from: 'recipes', field: 'route', target: { kind: 'axis', axis: 'routes' }, via: 'ops.rel.route', optional: true },
|
|
179
|
+
{ from: 'recipes', field: 'inputs[].material', target: { kind: 'axis', axis: 'materials' }, via: 'ops.rel.input' },
|
|
180
|
+
{ from: 'recipes', field: 'outputs[].material', target: { kind: 'axis', axis: 'materials' }, via: 'ops.rel.output' },
|
|
181
|
+
{ from: 'routes', field: 'steps[]', target: { kind: 'axis', axis: 'operations' }, via: 'ops.rel.step' },
|
|
170
182
|
/*
|
|
171
183
|
* 일정·실적의 관계 — **관측 관계다.** 선언 관계는 "그렇게 만들기로 했다" 이고, 이것은 "그렇게
|
|
172
184
|
* 일어났다" 다. 관계가 없으면 이 두 축은 지도에서 연결되지 않은 카드가 된다(그러면 걸어 들어갈 수 없다).
|
|
@@ -176,14 +188,14 @@ export const TWIN_RELATIONS = [
|
|
|
176
188
|
* 것은 `productionSpec.binding` 이고 그 해소는 호스트가 한다 — 자리·구역과 같은 `external` 이다.
|
|
177
189
|
* 축을 직접 가리키게 적으면 없는 필드를 가리키는 선언이 된다(계약을 보고 고쳤다).
|
|
178
190
|
*/
|
|
179
|
-
{ from: 'orders', field: 'lines[].gtin', target: { kind: 'external', entity: 'gs1.itemRef' }, via: '
|
|
180
|
-
{ from: 'tasks', field: 'orderId', target: { kind: 'axis', axis: 'orders' }, via: '
|
|
191
|
+
{ from: 'orders', field: 'lines[].gtin', target: { kind: 'external', entity: 'gs1.itemRef' }, via: 'ops.rel.item', optional: true },
|
|
192
|
+
{ from: 'tasks', field: 'orderId', target: { kind: 'axis', axis: 'orders' }, via: 'ops.rel.order', optional: true },
|
|
181
193
|
/* 작업의 `kind` 는 그 공정의 키다(라우트의 단계 이름 그대로 — `mes-kernel` 의 공정 목록이 그 증거). */
|
|
182
|
-
{ from: 'tasks', field: 'kind', target: { kind: 'axis', axis: 'operations' }, via: '
|
|
183
|
-
{ from: 'tasks', field: 'fromNode', target: { kind: 'axis', axis: 'locations' }, via: '
|
|
184
|
-
{ from: 'tasks', field: 'toNode', target: { kind: 'axis', axis: 'locations' }, via: '
|
|
185
|
-
{ from: 'tasks', field: 'resourceRef', target: { kind: 'axis', axis: 'equipment' }, via: '
|
|
186
|
-
{ from: 'tasks', field: 'personnel[]', target: { kind: 'axis', axis: 'persons' }, via: '
|
|
194
|
+
{ from: 'tasks', field: 'kind', target: { kind: 'axis', axis: 'operations' }, via: 'ops.rel.operation', optional: true },
|
|
195
|
+
{ from: 'tasks', field: 'fromNode', target: { kind: 'axis', axis: 'locations' }, via: 'ops.rel.from', optional: true },
|
|
196
|
+
{ from: 'tasks', field: 'toNode', target: { kind: 'axis', axis: 'locations' }, via: 'ops.rel.at', optional: true },
|
|
197
|
+
{ from: 'tasks', field: 'resourceRef', target: { kind: 'axis', axis: 'equipment' }, via: 'ops.rel.by', optional: true },
|
|
198
|
+
{ from: 'tasks', field: 'personnel[]', target: { kind: 'axis', axis: 'persons' }, via: 'ops.rel.crew', optional: true },
|
|
187
199
|
/*
|
|
188
200
|
* 물품의 관계 (2026-08-22) — 없으면 축이 **걸어 들어갈 수 없는 목록**이 된다.
|
|
189
201
|
*
|
|
@@ -193,15 +205,15 @@ export const TWIN_RELATIONS = [
|
|
|
193
205
|
* `parent` 는 **물품 축을 자기 자신으로** 가리킨다(팔레트에 담긴 상자 — EPCIS `AggregationEvent`).
|
|
194
206
|
* `carriedBy` 는 다른 축이다 — 반복사용 자산(GRAI)이 물류단위를 실어 나른다(§`FlowItem.carriedBy`).
|
|
195
207
|
*
|
|
196
|
-
* 관계 이름은 **소문자 한 낱말**이다(`
|
|
208
|
+
* 관계 이름은 **소문자 한 낱말**이다(`ops.rel.<name>`) — 기존 열여덟 개가 그 규율이고 시험이 지킨다.
|
|
197
209
|
*
|
|
198
210
|
* 품목(`gtin`)은 오더와 **같은 규율**이다: 자재 키가 아니라 GS1 품목 참조이므로 축을 직접 가리키지
|
|
199
211
|
* 않고 `external` 로 둔다. 축을 가리키게 적으면 없는 필드를 가리키는 선언이 된다.
|
|
200
212
|
*/
|
|
201
|
-
{ from: 'items', field: 'location', target: { kind: 'axis', axis: 'locations' }, via: '
|
|
202
|
-
{ from: 'items', field: 'parent', target: { kind: 'axis', axis: 'items' }, via: '
|
|
203
|
-
{ from: 'items', field: 'carriedBy', target: { kind: 'axis', axis: 'assets' }, via: '
|
|
204
|
-
{ from: 'items', field: 'gtin', target: { kind: 'external', entity: 'gs1.itemRef' }, via: '
|
|
213
|
+
{ from: 'items', field: 'location', target: { kind: 'axis', axis: 'locations' }, via: 'ops.rel.at' },
|
|
214
|
+
{ from: 'items', field: 'parent', target: { kind: 'axis', axis: 'items' }, via: 'ops.rel.parent', optional: true },
|
|
215
|
+
{ from: 'items', field: 'carriedBy', target: { kind: 'axis', axis: 'assets' }, via: 'ops.rel.asset', optional: true },
|
|
216
|
+
{ from: 'items', field: 'gtin', target: { kind: 'external', entity: 'gs1.itemRef' }, via: 'ops.rel.item', optional: true },
|
|
205
217
|
/*
|
|
206
218
|
* 자격을 검증한 시험 — **여덟 갈래.** 자원(개체)과 등급 양쪽이 가리킨다: 표준이 그 둘 모두에 이
|
|
207
219
|
* 참조를 두었기 때문이다(개체는 "이 사람이 통과했다", 등급은 "이 자격은 이 시험을 요구한다").
|
|
@@ -209,17 +221,17 @@ export const TWIN_RELATIONS = [
|
|
|
209
221
|
* **자리(`locations`)에는 없다** — 표준이 자리를 시험 대상으로 두지 않았고 그 비대칭은 표준의
|
|
210
222
|
* 판단이다(`contract.ts` 가 "우리도 자리에 넣지 않는다" 고 적었다).
|
|
211
223
|
*/
|
|
212
|
-
{ from: 'persons', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: '
|
|
213
|
-
{ from: 'personnelClasses', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: '
|
|
214
|
-
{ from: 'equipment', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: '
|
|
215
|
-
{ from: 'equipmentClasses', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: '
|
|
216
|
-
{ from: 'assets', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: '
|
|
217
|
-
{ from: 'assetClasses', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: '
|
|
218
|
-
{ from: 'materialDefinitions', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: '
|
|
219
|
-
{ from: 'materialClasses', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: '
|
|
224
|
+
{ from: 'persons', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'ops.rel.test', optional: true },
|
|
225
|
+
{ from: 'personnelClasses', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'ops.rel.test', optional: true },
|
|
226
|
+
{ from: 'equipment', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'ops.rel.test', optional: true },
|
|
227
|
+
{ from: 'equipmentClasses', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'ops.rel.test', optional: true },
|
|
228
|
+
{ from: 'assets', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'ops.rel.test', optional: true },
|
|
229
|
+
{ from: 'assetClasses', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'ops.rel.test', optional: true },
|
|
230
|
+
{ from: 'materialDefinitions', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'ops.rel.test', optional: true },
|
|
231
|
+
{ from: 'materialClasses', field: 'testSpecificationIds[]', target: { kind: 'axis', axis: 'testSpecifications' }, via: 'ops.rel.test', optional: true },
|
|
220
232
|
/* 공정이 도는 자리·자원 — 항목이 아니라 **타입**을 가리킨다 */
|
|
221
|
-
{ from: 'operations', field: 'locationType', target: { kind: 'type', role: 'location' }, via: '
|
|
222
|
-
{ from: 'operations', field: 'resourceType', target: { kind: 'type', role: 'equipment' }, via: '
|
|
233
|
+
{ from: 'operations', field: 'locationType', target: { kind: 'type', role: 'location' }, via: 'ops.rel.at', optional: true },
|
|
234
|
+
{ from: 'operations', field: 'resourceType', target: { kind: 'type', role: 'equipment' }, via: 'ops.rel.by', optional: true }
|
|
223
235
|
];
|
|
224
236
|
/**
|
|
225
237
|
* 오늘 **실제로 짝이 서는 것**만 선언한다. 그럴듯한 속성을 미리 적어 두면 화면이 빈칸을 줄줄이
|
package/dist/reliability.d.ts
CHANGED
|
@@ -42,3 +42,38 @@ export interface Mtbf {
|
|
|
42
42
|
* 사라지고, 그러면 이 수가 실제보다 좋아진다 — 자료가 나쁠수록 수가 좋아지는 쪽은 두지 않는다.
|
|
43
43
|
*/
|
|
44
44
|
export declare function computeMtbf(input: MtbfInput): Mtbf;
|
|
45
|
+
export interface MttrInput {
|
|
46
|
+
/** 어느 설비의. */
|
|
47
|
+
equipmentId: string;
|
|
48
|
+
/** 그 설비의 상태 구간들. 순서는 상관없다 — 여기서 시각으로 세운다. */
|
|
49
|
+
periods: readonly ReliabilityPeriod[];
|
|
50
|
+
}
|
|
51
|
+
/** 낼 수 없는 이유 — 「0」이나 「무한」으로 답하지 않는다. */
|
|
52
|
+
export type MttrMissing =
|
|
53
|
+
/** 고장이 없다 — 나눌 수가 0이다. 고칠 일이 없었던 것이지 복구가 빠른 것이 아니다. */
|
|
54
|
+
'no-failure'
|
|
55
|
+
/** 모르는 상태 낱말이 있다 — 조용히 빼면 고장이 사라진다. */
|
|
56
|
+
| 'unknown-status'
|
|
57
|
+
/** 길이가 없거나 시각을 읽을 수 없는 구간이 있다. */
|
|
58
|
+
| 'invalid-period';
|
|
59
|
+
export interface Mttr {
|
|
60
|
+
equipmentId: string;
|
|
61
|
+
/** 평균 복구 시간(ms). **낼 수 없으면 없다.** */
|
|
62
|
+
mttrMs?: number;
|
|
63
|
+
/** 분자 — `down` 구간 길이의 합. */
|
|
64
|
+
downMs: number;
|
|
65
|
+
/** 분모 — **고장으로 들어간 횟수**(이어진 `down` 은 한 번). MTBF 와 같은 수다. */
|
|
66
|
+
failures: number;
|
|
67
|
+
/** 받은 `down` 구간의 수 — `failures` 와 다르면 이어진 것이 있었다. */
|
|
68
|
+
downPeriods: number;
|
|
69
|
+
/** 옮길 수 없던 상태 낱말들 — 무엇이 왔는지 말한다. */
|
|
70
|
+
unknownStatuses: string[];
|
|
71
|
+
missing: MttrMissing[];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 이 설비의 MTTR.
|
|
75
|
+
*
|
|
76
|
+
* **모르는 상태를 만나면 답하지 않는다.** MTBF 와 같은 이유다 — 그 구간이 고장이었다면 빼는 순간
|
|
77
|
+
* 복구 시간이 짧아 보인다. 자료가 나쁠수록 수가 좋아지는 쪽은 두지 않는다.
|
|
78
|
+
*/
|
|
79
|
+
export declare function computeMttr(input: MttrInput): Mttr;
|
package/dist/reliability.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* **MTBF** — ISO 22400-2. 고장 사이에 얼마나
|
|
2
|
+
* **MTBF · MTTR** — ISO 22400-2. 고장 사이에 얼마나 돌았나 · 고치는 데 얼마나 걸렸나.
|
|
3
|
+
*
|
|
4
|
+
* 표준이 둘을 **짝으로** 정의한다. 한쪽만 두면 소비처가 나머지를 자기 식으로 짓고, 그러면 같은
|
|
5
|
+
* 설비가 화면마다 다른 수를 갖는다 — 이 파일이 애초에 존재하는 이유가 그것이다.
|
|
3
6
|
*
|
|
4
7
|
* ── 왜 구간을 받나 (집계 값이 아니라) ────────────────────────────────────────
|
|
5
8
|
* `computeMtbf(가동시간, 고장횟수)` 로 두면 **「고장 횟수」를 부르는 쪽이 센다.** 그러면 세는 방법이
|
|
@@ -42,15 +45,22 @@ const at = (t) => (typeof t === 'string' ? Date.parse(t) : NaN);
|
|
|
42
45
|
* **모르는 상태를 만나면 답하지 않는다.** 빼고 계산하면 그 구간이 고장이었을 때 고장이 조용히
|
|
43
46
|
* 사라지고, 그러면 이 수가 실제보다 좋아진다 — 자료가 나쁠수록 수가 좋아지는 쪽은 두지 않는다.
|
|
44
47
|
*/
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
/**
|
|
49
|
+
* 구간을 한 번 훑어 두 산출식이 쓸 것을 다 센다.
|
|
50
|
+
*
|
|
51
|
+
* **두 함수가 따로 걸으면 안 된다** — 「이어진 `down` 은 한 번」 규칙이 두 곳에 살게 되고, 한 곳만
|
|
52
|
+
* 고쳐지는 날 같은 설비의 MTBF 와 MTTR 이 서로 어긋난다. 그 둘은 같은 고장 횟수로 나눈 값이므로
|
|
53
|
+
* 어긋나면 둘 중 하나가 반드시 틀렸는데, 어느 쪽인지 아무도 모른다.
|
|
54
|
+
*/
|
|
55
|
+
function scanPeriods(periods) {
|
|
48
56
|
const unknown = new Set();
|
|
49
57
|
/* 시각으로 세운다 — 받은 순서를 믿지 않는다. 이어짐 판정이 순서에 걸린다. */
|
|
50
|
-
const sorted = [...(
|
|
58
|
+
const sorted = [...(periods ?? [])]
|
|
51
59
|
.map(p => ({ status: normalizeEquipmentStatus(p?.status), said: String(p?.status ?? ''), from: at(p?.from), to: at(p?.to) }))
|
|
52
60
|
.sort((a, b) => a.from - b.from);
|
|
53
61
|
let operatingMs = 0;
|
|
62
|
+
/** 고친 시간의 합 — MTTR 의 분자. */
|
|
63
|
+
let downMs = 0;
|
|
54
64
|
let failures = 0;
|
|
55
65
|
let downPeriods = 0;
|
|
56
66
|
let invalid = false;
|
|
@@ -74,6 +84,7 @@ export function computeMtbf(input) {
|
|
|
74
84
|
if (p.status === 'busy')
|
|
75
85
|
operatingMs += p.to - p.from;
|
|
76
86
|
if (p.status === 'down') {
|
|
87
|
+
downMs += p.to - p.from;
|
|
77
88
|
downPeriods++;
|
|
78
89
|
if (!wasDown)
|
|
79
90
|
failures++;
|
|
@@ -83,23 +94,61 @@ export function computeMtbf(input) {
|
|
|
83
94
|
wasDown = false;
|
|
84
95
|
}
|
|
85
96
|
}
|
|
86
|
-
|
|
97
|
+
return { operatingMs, downMs, failures, downPeriods, unknownStatuses: [...unknown], invalid };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* 이 설비의 MTBF.
|
|
101
|
+
*
|
|
102
|
+
* **모르는 상태를 만나면 답하지 않는다.** 빼고 계산하면 그 구간이 고장이었을 때 고장이 조용히
|
|
103
|
+
* 사라지고, 그러면 이 수가 실제보다 좋아진다 — 자료가 나쁠수록 수가 좋아지는 쪽은 두지 않는다.
|
|
104
|
+
*/
|
|
105
|
+
export function computeMtbf(input) {
|
|
106
|
+
const s = scanPeriods(input.periods);
|
|
107
|
+
const missing = [];
|
|
108
|
+
if (s.invalid)
|
|
87
109
|
missing.push('invalid-period');
|
|
88
|
-
if (
|
|
110
|
+
if (s.unknownStatuses.length)
|
|
89
111
|
missing.push('unknown-status');
|
|
90
|
-
if (operatingMs <= 0)
|
|
112
|
+
if (s.operatingMs <= 0)
|
|
91
113
|
missing.push('no-operating-time');
|
|
92
|
-
if (failures === 0)
|
|
114
|
+
if (s.failures === 0)
|
|
115
|
+
missing.push('no-failure');
|
|
116
|
+
const out = {
|
|
117
|
+
equipmentId: input.equipmentId,
|
|
118
|
+
operatingMs: s.operatingMs,
|
|
119
|
+
failures: s.failures,
|
|
120
|
+
downPeriods: s.downPeriods,
|
|
121
|
+
unknownStatuses: s.unknownStatuses,
|
|
122
|
+
missing
|
|
123
|
+
};
|
|
124
|
+
if (missing.length)
|
|
125
|
+
return out;
|
|
126
|
+
return { ...out, mtbfMs: s.operatingMs / s.failures };
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* 이 설비의 MTTR.
|
|
130
|
+
*
|
|
131
|
+
* **모르는 상태를 만나면 답하지 않는다.** MTBF 와 같은 이유다 — 그 구간이 고장이었다면 빼는 순간
|
|
132
|
+
* 복구 시간이 짧아 보인다. 자료가 나쁠수록 수가 좋아지는 쪽은 두지 않는다.
|
|
133
|
+
*/
|
|
134
|
+
export function computeMttr(input) {
|
|
135
|
+
const s = scanPeriods(input.periods);
|
|
136
|
+
const missing = [];
|
|
137
|
+
if (s.invalid)
|
|
138
|
+
missing.push('invalid-period');
|
|
139
|
+
if (s.unknownStatuses.length)
|
|
140
|
+
missing.push('unknown-status');
|
|
141
|
+
if (s.failures === 0)
|
|
93
142
|
missing.push('no-failure');
|
|
94
143
|
const out = {
|
|
95
|
-
equipmentId,
|
|
96
|
-
|
|
97
|
-
failures,
|
|
98
|
-
downPeriods,
|
|
99
|
-
unknownStatuses:
|
|
144
|
+
equipmentId: input.equipmentId,
|
|
145
|
+
downMs: s.downMs,
|
|
146
|
+
failures: s.failures,
|
|
147
|
+
downPeriods: s.downPeriods,
|
|
148
|
+
unknownStatuses: s.unknownStatuses,
|
|
100
149
|
missing
|
|
101
150
|
};
|
|
102
151
|
if (missing.length)
|
|
103
152
|
return out;
|
|
104
|
-
return { ...out,
|
|
153
|
+
return { ...out, mttrMs: s.downMs / s.failures };
|
|
105
154
|
}
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -62,6 +62,7 @@ __export(index_exports, {
|
|
|
62
62
|
OP_PARAM: () => OP_PARAM,
|
|
63
63
|
ORDER_TERMINAL_STATUS: () => ORDER_TERMINAL_STATUS,
|
|
64
64
|
PRIORITY_UNSET: () => PRIORITY_UNSET,
|
|
65
|
+
RESOURCE_KIND: () => RESOURCE_KIND,
|
|
65
66
|
RETIRED_VOCABULARY: () => RETIRED_VOCABULARY,
|
|
66
67
|
SCHEDULE_STATUS: () => SCHEDULE_STATUS,
|
|
67
68
|
TWIN_AXES: () => TWIN_AXES,
|
|
@@ -103,6 +104,7 @@ __export(index_exports, {
|
|
|
103
104
|
commandsOf: () => commandsOf,
|
|
104
105
|
computeFirstPassYield: () => computeFirstPassYield,
|
|
105
106
|
computeMtbf: () => computeMtbf,
|
|
107
|
+
computeMttr: () => computeMttr,
|
|
106
108
|
computeOee: () => computeOee,
|
|
107
109
|
conversionFactorOf: () => conversionFactorOf,
|
|
108
110
|
criterionSaysNothing: () => criterionSaysNothing,
|
|
@@ -118,6 +120,7 @@ __export(index_exports, {
|
|
|
118
120
|
graiUri: () => graiUri,
|
|
119
121
|
gs1KeyDigitViolation: () => gs1KeyDigitViolation,
|
|
120
122
|
hierarchyOf: () => hierarchyOf,
|
|
123
|
+
homeOf: () => homeOf,
|
|
121
124
|
identityGroundingOf: () => identityGroundingOf,
|
|
122
125
|
inWorkCalendar: () => inWorkCalendar,
|
|
123
126
|
inWorkCalendarAt: () => inWorkCalendarAt,
|
|
@@ -151,6 +154,7 @@ __export(index_exports, {
|
|
|
151
154
|
isOperationalRecord: () => isOperationalRecord,
|
|
152
155
|
isOrderTerminal: () => isOrderTerminal,
|
|
153
156
|
isPlannedStopStatus: () => isPlannedStopStatus,
|
|
157
|
+
isResourceKind: () => isResourceKind,
|
|
154
158
|
isTransformationRecord: () => isTransformationRecord,
|
|
155
159
|
isoDurationHours: () => isoDurationHours,
|
|
156
160
|
itemKeyOf: () => itemKeyOf,
|
|
@@ -203,6 +207,7 @@ __export(index_exports, {
|
|
|
203
207
|
ssccUri: () => ssccUri,
|
|
204
208
|
stateFieldsOf: () => stateFieldsOf,
|
|
205
209
|
subLotIdOf: () => subLotIdOf,
|
|
210
|
+
tenseOf: () => tenseOf,
|
|
206
211
|
testEvidenceGaps: () => testEvidenceGaps,
|
|
207
212
|
testPassedAt: () => testPassedAt,
|
|
208
213
|
transactionEvent: () => transactionEvent,
|
|
@@ -825,6 +830,10 @@ function observationAt(observations, propertyId, at2) {
|
|
|
825
830
|
}
|
|
826
831
|
return best;
|
|
827
832
|
}
|
|
833
|
+
function homeOf(e) {
|
|
834
|
+
const id = e?.homeLocation;
|
|
835
|
+
return id ? { id, kind: e?.homeLocationKind ?? "location" } : void 0;
|
|
836
|
+
}
|
|
828
837
|
var OP_EVENT = {
|
|
829
838
|
task: "task.status",
|
|
830
839
|
equipment: "equipment.status",
|
|
@@ -1024,6 +1033,10 @@ var EQUIPMENT_STATUS = ["busy", "setup", "down", "idle", "planned-stop"];
|
|
|
1024
1033
|
function isEquipmentStatus(value) {
|
|
1025
1034
|
return typeof value === "string" && EQUIPMENT_STATUS.includes(value);
|
|
1026
1035
|
}
|
|
1036
|
+
var RESOURCE_KIND = ["equipment", "personnel", "material", "asset"];
|
|
1037
|
+
function isResourceKind(value) {
|
|
1038
|
+
return typeof value === "string" && RESOURCE_KIND.includes(value);
|
|
1039
|
+
}
|
|
1027
1040
|
function normalizeEquipmentStatus(value) {
|
|
1028
1041
|
const said = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
1029
1042
|
return isEquipmentStatus(said) ? said : void 0;
|
|
@@ -1860,23 +1873,26 @@ var TWIN_AXES = [
|
|
|
1860
1873
|
systems: ["ems"]
|
|
1861
1874
|
}
|
|
1862
1875
|
];
|
|
1876
|
+
function tenseOf(r) {
|
|
1877
|
+
return r?.tense;
|
|
1878
|
+
}
|
|
1863
1879
|
var TWIN_RELATIONS = [
|
|
1864
1880
|
/* 자리가 속한 구역 — **board 밖**(호스트의 `TwinArea`). 해소는 호스트가 한다. */
|
|
1865
|
-
{ from: "locations", field: "parentId", target: { kind: "external", entity: "space.area" }, via: "
|
|
1881
|
+
{ from: "locations", field: "parentId", target: { kind: "external", entity: "space.area" }, via: "ops.rel.area", optional: true },
|
|
1866
1882
|
/* 자원이 사는 자리 */
|
|
1867
|
-
{ from: "equipment", field: "homeLocation", target: { kind: "axis", axis: "locations" }, via: "
|
|
1868
|
-
{ from: "persons", field: "homeLocation", target: { kind: "axis", axis: "locations" }, via: "
|
|
1869
|
-
{ from: "assets", field: "homeLocation", target: { kind: "axis", axis: "locations" }, via: "
|
|
1883
|
+
{ from: "equipment", field: "homeLocation", target: { kind: "axis", axis: "locations" }, via: "ops.rel.home", optional: true },
|
|
1884
|
+
{ from: "persons", field: "homeLocation", target: { kind: "axis", axis: "locations" }, via: "ops.rel.home", optional: true },
|
|
1885
|
+
{ from: "assets", field: "homeLocation", target: { kind: "axis", axis: "locations" }, via: "ops.rel.home", optional: true },
|
|
1870
1886
|
/* 자원의 등급 — 설비만 `kind`(타입)로 가고 사람·자산은 등급 목록으로 간다. 표준의 비대칭 그대로. */
|
|
1871
|
-
{ from: "persons", field: "personnelClassIds[]", target: { kind: "axis", axis: "personnelClasses" }, via: "
|
|
1872
|
-
{ from: "assets", field: "assetClassIds[]", target: { kind: "axis", axis: "assetClasses" }, via: "
|
|
1873
|
-
{ from: "equipment", field: "kind", target: { kind: "type", role: "equipment" }, via: "
|
|
1874
|
-
{ from: "locations", field: "type", target: { kind: "type", role: "location" }, via: "
|
|
1887
|
+
{ from: "persons", field: "personnelClassIds[]", target: { kind: "axis", axis: "personnelClasses" }, via: "ops.rel.class", optional: true },
|
|
1888
|
+
{ from: "assets", field: "assetClassIds[]", target: { kind: "axis", axis: "assetClasses" }, via: "ops.rel.class", optional: true },
|
|
1889
|
+
{ from: "equipment", field: "kind", target: { kind: "type", role: "equipment" }, via: "ops.rel.type" },
|
|
1890
|
+
{ from: "locations", field: "type", target: { kind: "type", role: "location" }, via: "ops.rel.type" },
|
|
1875
1891
|
/* 생산 — 레시피에서 시작해 라우트·공정·품목으로 퍼진다 */
|
|
1876
|
-
{ from: "recipes", field: "route", target: { kind: "axis", axis: "routes" }, via: "
|
|
1877
|
-
{ from: "recipes", field: "inputs[].material", target: { kind: "axis", axis: "materials" }, via: "
|
|
1878
|
-
{ from: "recipes", field: "outputs[].material", target: { kind: "axis", axis: "materials" }, via: "
|
|
1879
|
-
{ from: "routes", field: "steps[]", target: { kind: "axis", axis: "operations" }, via: "
|
|
1892
|
+
{ from: "recipes", field: "route", target: { kind: "axis", axis: "routes" }, via: "ops.rel.route", optional: true },
|
|
1893
|
+
{ from: "recipes", field: "inputs[].material", target: { kind: "axis", axis: "materials" }, via: "ops.rel.input" },
|
|
1894
|
+
{ from: "recipes", field: "outputs[].material", target: { kind: "axis", axis: "materials" }, via: "ops.rel.output" },
|
|
1895
|
+
{ from: "routes", field: "steps[]", target: { kind: "axis", axis: "operations" }, via: "ops.rel.step" },
|
|
1880
1896
|
/*
|
|
1881
1897
|
* 일정·실적의 관계 — **관측 관계다.** 선언 관계는 "그렇게 만들기로 했다" 이고, 이것은 "그렇게
|
|
1882
1898
|
* 일어났다" 다. 관계가 없으면 이 두 축은 지도에서 연결되지 않은 카드가 된다(그러면 걸어 들어갈 수 없다).
|
|
@@ -1886,14 +1902,14 @@ var TWIN_RELATIONS = [
|
|
|
1886
1902
|
* 것은 `productionSpec.binding` 이고 그 해소는 호스트가 한다 — 자리·구역과 같은 `external` 이다.
|
|
1887
1903
|
* 축을 직접 가리키게 적으면 없는 필드를 가리키는 선언이 된다(계약을 보고 고쳤다).
|
|
1888
1904
|
*/
|
|
1889
|
-
{ from: "orders", field: "lines[].gtin", target: { kind: "external", entity: "gs1.itemRef" }, via: "
|
|
1890
|
-
{ from: "tasks", field: "orderId", target: { kind: "axis", axis: "orders" }, via: "
|
|
1905
|
+
{ from: "orders", field: "lines[].gtin", target: { kind: "external", entity: "gs1.itemRef" }, via: "ops.rel.item", optional: true },
|
|
1906
|
+
{ from: "tasks", field: "orderId", target: { kind: "axis", axis: "orders" }, via: "ops.rel.order", optional: true },
|
|
1891
1907
|
/* 작업의 `kind` 는 그 공정의 키다(라우트의 단계 이름 그대로 — `mes-kernel` 의 공정 목록이 그 증거). */
|
|
1892
|
-
{ from: "tasks", field: "kind", target: { kind: "axis", axis: "operations" }, via: "
|
|
1893
|
-
{ from: "tasks", field: "fromNode", target: { kind: "axis", axis: "locations" }, via: "
|
|
1894
|
-
{ from: "tasks", field: "toNode", target: { kind: "axis", axis: "locations" }, via: "
|
|
1895
|
-
{ from: "tasks", field: "resourceRef", target: { kind: "axis", axis: "equipment" }, via: "
|
|
1896
|
-
{ from: "tasks", field: "personnel[]", target: { kind: "axis", axis: "persons" }, via: "
|
|
1908
|
+
{ from: "tasks", field: "kind", target: { kind: "axis", axis: "operations" }, via: "ops.rel.operation", optional: true },
|
|
1909
|
+
{ from: "tasks", field: "fromNode", target: { kind: "axis", axis: "locations" }, via: "ops.rel.from", optional: true },
|
|
1910
|
+
{ from: "tasks", field: "toNode", target: { kind: "axis", axis: "locations" }, via: "ops.rel.at", optional: true },
|
|
1911
|
+
{ from: "tasks", field: "resourceRef", target: { kind: "axis", axis: "equipment" }, via: "ops.rel.by", optional: true },
|
|
1912
|
+
{ from: "tasks", field: "personnel[]", target: { kind: "axis", axis: "persons" }, via: "ops.rel.crew", optional: true },
|
|
1897
1913
|
/*
|
|
1898
1914
|
* 물품의 관계 (2026-08-22) — 없으면 축이 **걸어 들어갈 수 없는 목록**이 된다.
|
|
1899
1915
|
*
|
|
@@ -1903,15 +1919,15 @@ var TWIN_RELATIONS = [
|
|
|
1903
1919
|
* `parent` 는 **물품 축을 자기 자신으로** 가리킨다(팔레트에 담긴 상자 — EPCIS `AggregationEvent`).
|
|
1904
1920
|
* `carriedBy` 는 다른 축이다 — 반복사용 자산(GRAI)이 물류단위를 실어 나른다(§`FlowItem.carriedBy`).
|
|
1905
1921
|
*
|
|
1906
|
-
* 관계 이름은 **소문자 한 낱말**이다(`
|
|
1922
|
+
* 관계 이름은 **소문자 한 낱말**이다(`ops.rel.<name>`) — 기존 열여덟 개가 그 규율이고 시험이 지킨다.
|
|
1907
1923
|
*
|
|
1908
1924
|
* 품목(`gtin`)은 오더와 **같은 규율**이다: 자재 키가 아니라 GS1 품목 참조이므로 축을 직접 가리키지
|
|
1909
1925
|
* 않고 `external` 로 둔다. 축을 가리키게 적으면 없는 필드를 가리키는 선언이 된다.
|
|
1910
1926
|
*/
|
|
1911
|
-
{ from: "items", field: "location", target: { kind: "axis", axis: "locations" }, via: "
|
|
1912
|
-
{ from: "items", field: "parent", target: { kind: "axis", axis: "items" }, via: "
|
|
1913
|
-
{ from: "items", field: "carriedBy", target: { kind: "axis", axis: "assets" }, via: "
|
|
1914
|
-
{ from: "items", field: "gtin", target: { kind: "external", entity: "gs1.itemRef" }, via: "
|
|
1927
|
+
{ from: "items", field: "location", target: { kind: "axis", axis: "locations" }, via: "ops.rel.at" },
|
|
1928
|
+
{ from: "items", field: "parent", target: { kind: "axis", axis: "items" }, via: "ops.rel.parent", optional: true },
|
|
1929
|
+
{ from: "items", field: "carriedBy", target: { kind: "axis", axis: "assets" }, via: "ops.rel.asset", optional: true },
|
|
1930
|
+
{ from: "items", field: "gtin", target: { kind: "external", entity: "gs1.itemRef" }, via: "ops.rel.item", optional: true },
|
|
1915
1931
|
/*
|
|
1916
1932
|
* 자격을 검증한 시험 — **여덟 갈래.** 자원(개체)과 등급 양쪽이 가리킨다: 표준이 그 둘 모두에 이
|
|
1917
1933
|
* 참조를 두었기 때문이다(개체는 "이 사람이 통과했다", 등급은 "이 자격은 이 시험을 요구한다").
|
|
@@ -1919,17 +1935,17 @@ var TWIN_RELATIONS = [
|
|
|
1919
1935
|
* **자리(`locations`)에는 없다** — 표준이 자리를 시험 대상으로 두지 않았고 그 비대칭은 표준의
|
|
1920
1936
|
* 판단이다(`contract.ts` 가 "우리도 자리에 넣지 않는다" 고 적었다).
|
|
1921
1937
|
*/
|
|
1922
|
-
{ from: "persons", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "
|
|
1923
|
-
{ from: "personnelClasses", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "
|
|
1924
|
-
{ from: "equipment", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "
|
|
1925
|
-
{ from: "equipmentClasses", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "
|
|
1926
|
-
{ from: "assets", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "
|
|
1927
|
-
{ from: "assetClasses", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "
|
|
1928
|
-
{ from: "materialDefinitions", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "
|
|
1929
|
-
{ from: "materialClasses", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "
|
|
1938
|
+
{ from: "persons", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "ops.rel.test", optional: true },
|
|
1939
|
+
{ from: "personnelClasses", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "ops.rel.test", optional: true },
|
|
1940
|
+
{ from: "equipment", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "ops.rel.test", optional: true },
|
|
1941
|
+
{ from: "equipmentClasses", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "ops.rel.test", optional: true },
|
|
1942
|
+
{ from: "assets", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "ops.rel.test", optional: true },
|
|
1943
|
+
{ from: "assetClasses", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "ops.rel.test", optional: true },
|
|
1944
|
+
{ from: "materialDefinitions", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "ops.rel.test", optional: true },
|
|
1945
|
+
{ from: "materialClasses", field: "testSpecificationIds[]", target: { kind: "axis", axis: "testSpecifications" }, via: "ops.rel.test", optional: true },
|
|
1930
1946
|
/* 공정이 도는 자리·자원 — 항목이 아니라 **타입**을 가리킨다 */
|
|
1931
|
-
{ from: "operations", field: "locationType", target: { kind: "type", role: "location" }, via: "
|
|
1932
|
-
{ from: "operations", field: "resourceType", target: { kind: "type", role: "equipment" }, via: "
|
|
1947
|
+
{ from: "operations", field: "locationType", target: { kind: "type", role: "location" }, via: "ops.rel.at", optional: true },
|
|
1948
|
+
{ from: "operations", field: "resourceType", target: { kind: "type", role: "equipment" }, via: "ops.rel.by", optional: true }
|
|
1933
1949
|
];
|
|
1934
1950
|
var TWIN_PROPERTIES = [
|
|
1935
1951
|
/* 공정 — 선언(ISO 8601 기간)과 관측(저널에서 계산한 분포)이 둘 다 있는 유일한 짝. */
|
|
@@ -4070,12 +4086,11 @@ function rolledThroughputYield(steps) {
|
|
|
4070
4086
|
|
|
4071
4087
|
// src/reliability.ts
|
|
4072
4088
|
var at = (t) => typeof t === "string" ? Date.parse(t) : NaN;
|
|
4073
|
-
function
|
|
4074
|
-
const { equipmentId } = input;
|
|
4075
|
-
const missing = [];
|
|
4089
|
+
function scanPeriods(periods) {
|
|
4076
4090
|
const unknown = /* @__PURE__ */ new Set();
|
|
4077
|
-
const sorted = [...
|
|
4091
|
+
const sorted = [...periods ?? []].map((p) => ({ status: normalizeEquipmentStatus(p?.status), said: String(p?.status ?? ""), from: at(p?.from), to: at(p?.to) })).sort((a, b) => a.from - b.from);
|
|
4078
4092
|
let operatingMs = 0;
|
|
4093
|
+
let downMs = 0;
|
|
4079
4094
|
let failures = 0;
|
|
4080
4095
|
let downPeriods = 0;
|
|
4081
4096
|
let invalid = false;
|
|
@@ -4092,6 +4107,7 @@ function computeMtbf(input) {
|
|
|
4092
4107
|
}
|
|
4093
4108
|
if (p.status === "busy") operatingMs += p.to - p.from;
|
|
4094
4109
|
if (p.status === "down") {
|
|
4110
|
+
downMs += p.to - p.from;
|
|
4095
4111
|
downPeriods++;
|
|
4096
4112
|
if (!wasDown) failures++;
|
|
4097
4113
|
wasDown = true;
|
|
@@ -4099,20 +4115,42 @@ function computeMtbf(input) {
|
|
|
4099
4115
|
wasDown = false;
|
|
4100
4116
|
}
|
|
4101
4117
|
}
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4118
|
+
return { operatingMs, downMs, failures, downPeriods, unknownStatuses: [...unknown], invalid };
|
|
4119
|
+
}
|
|
4120
|
+
function computeMtbf(input) {
|
|
4121
|
+
const s = scanPeriods(input.periods);
|
|
4122
|
+
const missing = [];
|
|
4123
|
+
if (s.invalid) missing.push("invalid-period");
|
|
4124
|
+
if (s.unknownStatuses.length) missing.push("unknown-status");
|
|
4125
|
+
if (s.operatingMs <= 0) missing.push("no-operating-time");
|
|
4126
|
+
if (s.failures === 0) missing.push("no-failure");
|
|
4127
|
+
const out = {
|
|
4128
|
+
equipmentId: input.equipmentId,
|
|
4129
|
+
operatingMs: s.operatingMs,
|
|
4130
|
+
failures: s.failures,
|
|
4131
|
+
downPeriods: s.downPeriods,
|
|
4132
|
+
unknownStatuses: s.unknownStatuses,
|
|
4133
|
+
missing
|
|
4134
|
+
};
|
|
4135
|
+
if (missing.length) return out;
|
|
4136
|
+
return { ...out, mtbfMs: s.operatingMs / s.failures };
|
|
4137
|
+
}
|
|
4138
|
+
function computeMttr(input) {
|
|
4139
|
+
const s = scanPeriods(input.periods);
|
|
4140
|
+
const missing = [];
|
|
4141
|
+
if (s.invalid) missing.push("invalid-period");
|
|
4142
|
+
if (s.unknownStatuses.length) missing.push("unknown-status");
|
|
4143
|
+
if (s.failures === 0) missing.push("no-failure");
|
|
4106
4144
|
const out = {
|
|
4107
|
-
equipmentId,
|
|
4108
|
-
|
|
4109
|
-
failures,
|
|
4110
|
-
downPeriods,
|
|
4111
|
-
unknownStatuses:
|
|
4145
|
+
equipmentId: input.equipmentId,
|
|
4146
|
+
downMs: s.downMs,
|
|
4147
|
+
failures: s.failures,
|
|
4148
|
+
downPeriods: s.downPeriods,
|
|
4149
|
+
unknownStatuses: s.unknownStatuses,
|
|
4112
4150
|
missing
|
|
4113
4151
|
};
|
|
4114
4152
|
if (missing.length) return out;
|
|
4115
|
-
return { ...out,
|
|
4153
|
+
return { ...out, mttrMs: s.downMs / s.failures };
|
|
4116
4154
|
}
|
|
4117
4155
|
|
|
4118
4156
|
// src/erp.ts
|
|
@@ -4319,6 +4357,7 @@ function commandSpecGaps(specs, command) {
|
|
|
4319
4357
|
OP_PARAM,
|
|
4320
4358
|
ORDER_TERMINAL_STATUS,
|
|
4321
4359
|
PRIORITY_UNSET,
|
|
4360
|
+
RESOURCE_KIND,
|
|
4322
4361
|
RETIRED_VOCABULARY,
|
|
4323
4362
|
SCHEDULE_STATUS,
|
|
4324
4363
|
TWIN_AXES,
|
|
@@ -4360,6 +4399,7 @@ function commandSpecGaps(specs, command) {
|
|
|
4360
4399
|
commandsOf,
|
|
4361
4400
|
computeFirstPassYield,
|
|
4362
4401
|
computeMtbf,
|
|
4402
|
+
computeMttr,
|
|
4363
4403
|
computeOee,
|
|
4364
4404
|
conversionFactorOf,
|
|
4365
4405
|
criterionSaysNothing,
|
|
@@ -4375,6 +4415,7 @@ function commandSpecGaps(specs, command) {
|
|
|
4375
4415
|
graiUri,
|
|
4376
4416
|
gs1KeyDigitViolation,
|
|
4377
4417
|
hierarchyOf,
|
|
4418
|
+
homeOf,
|
|
4378
4419
|
identityGroundingOf,
|
|
4379
4420
|
inWorkCalendar,
|
|
4380
4421
|
inWorkCalendarAt,
|
|
@@ -4408,6 +4449,7 @@ function commandSpecGaps(specs, command) {
|
|
|
4408
4449
|
isOperationalRecord,
|
|
4409
4450
|
isOrderTerminal,
|
|
4410
4451
|
isPlannedStopStatus,
|
|
4452
|
+
isResourceKind,
|
|
4411
4453
|
isTransformationRecord,
|
|
4412
4454
|
isoDurationHours,
|
|
4413
4455
|
itemKeyOf,
|
|
@@ -4460,6 +4502,7 @@ function commandSpecGaps(specs, command) {
|
|
|
4460
4502
|
ssccUri,
|
|
4461
4503
|
stateFieldsOf,
|
|
4462
4504
|
subLotIdOf,
|
|
4505
|
+
tenseOf,
|
|
4463
4506
|
testEvidenceGaps,
|
|
4464
4507
|
testPassedAt,
|
|
4465
4508
|
transactionEvent,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@operato/ops-contract",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
4
4
|
"description": "Operations domain contract — the standard vocabulary that producers and readers agree on (EPCIS 2.0/GS1, ISA-95, IEC 61850/ISO 50001). Types, guards, validation. No state, no engine.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist-cjs/index.cjs",
|