@operato/ops-contract 0.9.3 → 0.9.5
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 +49 -2
- package/dist/contract.js +58 -0
- package/dist/domain-catalog.d.ts +39 -0
- package/dist/domain-catalog.js +12 -0
- package/dist/epcis.d.ts +18 -0
- package/dist/epcis.js +76 -4
- package/dist/operational-ingest.d.ts +28 -0
- package/dist/operational-ingest.js +76 -1
- package/dist/reliability.d.ts +35 -0
- package/dist/reliability.js +64 -15
- package/dist-cjs/index.cjs +132 -19
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -3172,6 +3172,38 @@ export declare const EQUIPMENT_STATUS: readonly ["busy", "setup", "down", "idle"
|
|
|
3172
3172
|
export type EquipmentStatus = (typeof EQUIPMENT_STATUS)[number];
|
|
3173
3173
|
/** 선언된 상태인가. */
|
|
3174
3174
|
export declare function isEquipmentStatus(value: unknown): value is EquipmentStatus;
|
|
3175
|
+
/**
|
|
3176
|
+
* **ISA-95 의 네 자원** — 「무엇이 자원인가」를 한 자리에서 말한다.
|
|
3177
|
+
*
|
|
3178
|
+
* ── 왜 필요한가 (MES 레인이 물음, 2026-09-05) ──────────────────────────────
|
|
3179
|
+
* 계약이 네 자원을 여러 곳에서 다루는데, **넷이라고 말하는 값이 없었다.** 그래서 소비처가 저마다
|
|
3180
|
+
* 손으로 든다 — 자원 하나에 걸린 것을 한자리에 보이는 화면(360)이 종류마다 무엇을 물을지 정하려면
|
|
3181
|
+
* 그 목록이 필요하고, 지금은 화면마다 적게 된다.
|
|
3182
|
+
*
|
|
3183
|
+
* 이미 갈라지기 시작했다: `CapacityAxis`(§`capacity.ts`)가 넷을 손으로 들고 있다.
|
|
3184
|
+
* `EQUIPMENT_STATUS` 가 정확히 같은 자리였다 — 계약에 있는데 소비처가 따로 선언하고 있었다.
|
|
3185
|
+
*
|
|
3186
|
+
* ── 왜 넷만인가 ────────────────────────────────────────────────────────────
|
|
3187
|
+
* 표준이 넷이라고 말한다(ISA-95 Part 1 §5: Personnel · Equipment · Material · Physical Asset).
|
|
3188
|
+
* 자리·공정·로트를 여기 섞고 싶은 힘이 있는데 — 화면이 그것들도 보여 주므로 — **섞으면 이 상수가
|
|
3189
|
+
* ISA-95 를 말하지 않게 된다.** 그러면 다음 사람이 이것을 근거로 쓸 수 없다.
|
|
3190
|
+
*
|
|
3191
|
+
* 자리 자원을 **담는 곳**이다. 자원이 아니다
|
|
3192
|
+
* 로트 자재의 **개체**다. 자재 종류와 다른 층이다
|
|
3193
|
+
* 공정 자원을 **쓰는 일**이다
|
|
3194
|
+
*
|
|
3195
|
+
* 화면이 그 셋도 필요하면 다른 이름으로 든다. 이 목록은 「무엇이 자원인가」에만 답한다.
|
|
3196
|
+
*
|
|
3197
|
+
* ── `CapacityAxis` 와 다르다 ───────────────────────────────────────────────
|
|
3198
|
+
* 그것은 **제약이 걸린 축**이고(무엇을 늘려야 하는가), 자리가 들어간다 — 자리가 모자라도 못 돌기
|
|
3199
|
+
* 때문이다. 자재는 없다(늘린다고 지금 나오는 것이 아니다). 둘은 겹치지만 같지 않다.
|
|
3200
|
+
*
|
|
3201
|
+
* 합치면 한 값이 두 물음에 답하게 되고, 그러면 어느 쪽 뜻으로 읽어야 하는지 알 수 없다.
|
|
3202
|
+
*/
|
|
3203
|
+
export declare const RESOURCE_KIND: readonly ["equipment", "personnel", "material", "asset"];
|
|
3204
|
+
export type ResourceKind = (typeof RESOURCE_KIND)[number];
|
|
3205
|
+
/** 선언된 자원 종류인가. */
|
|
3206
|
+
export declare function isResourceKind(value: unknown): value is ResourceKind;
|
|
3175
3207
|
/**
|
|
3176
3208
|
* 원본의 낱말을 우리 상태로 옮긴다 — **모르면 `undefined` 다.**
|
|
3177
3209
|
*
|
|
@@ -3269,11 +3301,26 @@ export interface QualityDelta {
|
|
|
3269
3301
|
* 표준 매핑은 커널이 이미 적어 두었다 — `MaterialDefinitionID` → `definitionId`,
|
|
3270
3302
|
* `MaterialUse` → `use`. 그 이름을 그대로 쓴다.
|
|
3271
3303
|
*/
|
|
3304
|
+
/**
|
|
3305
|
+
* **실적의 자재 쓰임** — 표준 `MaterialUse`. 들어갔나 나왔나 둘이다.
|
|
3306
|
+
*
|
|
3307
|
+
* 공정 **명세**의 쓰임(`OperationMaterial.use`)은 셋이다 — 거기에는 `consumable` 이 있다(쓰이지만
|
|
3308
|
+
* 제품에 남지 않는 것: 세척수·윤활유). **실적에는 그 낱말이 없다** — 일어난 일을 적는 자리이고,
|
|
3309
|
+
* 소모품이 실제로 들어간 것은 `consumed` 다.
|
|
3310
|
+
*
|
|
3311
|
+
* 둘을 한 어휘로 묶지 않는다. 묶으면 실적에 `consumable` 이 올 수 있게 되고, 그때 그것이
|
|
3312
|
+
* 「들어갔다」인지 「쓰였지만 안 남았다」인지 받는 쪽이 정해야 한다.
|
|
3313
|
+
*
|
|
3314
|
+
* 유입 게이트가 이 상수를 가리킨다(§`operational-ingest.ts` 의 `shapes`) — 낱말을 두 곳에 적으면
|
|
3315
|
+
* 한쪽만 늘어나는 날 게이트가 계약에 없는 낱말을 통과시킨다.
|
|
3316
|
+
*/
|
|
3317
|
+
export declare const MATERIAL_ACTUAL_USE: readonly ["consumed", "produced"];
|
|
3318
|
+
export type MaterialActualUse = (typeof MATERIAL_ACTUAL_USE)[number];
|
|
3272
3319
|
export interface MaterialActual {
|
|
3273
3320
|
/** 표준 `MaterialDefinitionID`. */
|
|
3274
3321
|
definitionId: string;
|
|
3275
|
-
/** 표준 `MaterialUse
|
|
3276
|
-
use:
|
|
3322
|
+
/** 표준 `MaterialUse` — 실적은 둘이다(§`MATERIAL_ACTUAL_USE`). */
|
|
3323
|
+
use: MaterialActualUse;
|
|
3277
3324
|
quantity: number;
|
|
3278
3325
|
uom?: string;
|
|
3279
3326
|
/**
|
package/dist/contract.js
CHANGED
|
@@ -1276,6 +1276,39 @@ export const EQUIPMENT_STATUS = ['busy', 'setup', 'down', 'idle', 'planned-stop'
|
|
|
1276
1276
|
export function isEquipmentStatus(value) {
|
|
1277
1277
|
return typeof value === 'string' && EQUIPMENT_STATUS.includes(value);
|
|
1278
1278
|
}
|
|
1279
|
+
/**
|
|
1280
|
+
* **ISA-95 의 네 자원** — 「무엇이 자원인가」를 한 자리에서 말한다.
|
|
1281
|
+
*
|
|
1282
|
+
* ── 왜 필요한가 (MES 레인이 물음, 2026-09-05) ──────────────────────────────
|
|
1283
|
+
* 계약이 네 자원을 여러 곳에서 다루는데, **넷이라고 말하는 값이 없었다.** 그래서 소비처가 저마다
|
|
1284
|
+
* 손으로 든다 — 자원 하나에 걸린 것을 한자리에 보이는 화면(360)이 종류마다 무엇을 물을지 정하려면
|
|
1285
|
+
* 그 목록이 필요하고, 지금은 화면마다 적게 된다.
|
|
1286
|
+
*
|
|
1287
|
+
* 이미 갈라지기 시작했다: `CapacityAxis`(§`capacity.ts`)가 넷을 손으로 들고 있다.
|
|
1288
|
+
* `EQUIPMENT_STATUS` 가 정확히 같은 자리였다 — 계약에 있는데 소비처가 따로 선언하고 있었다.
|
|
1289
|
+
*
|
|
1290
|
+
* ── 왜 넷만인가 ────────────────────────────────────────────────────────────
|
|
1291
|
+
* 표준이 넷이라고 말한다(ISA-95 Part 1 §5: Personnel · Equipment · Material · Physical Asset).
|
|
1292
|
+
* 자리·공정·로트를 여기 섞고 싶은 힘이 있는데 — 화면이 그것들도 보여 주므로 — **섞으면 이 상수가
|
|
1293
|
+
* ISA-95 를 말하지 않게 된다.** 그러면 다음 사람이 이것을 근거로 쓸 수 없다.
|
|
1294
|
+
*
|
|
1295
|
+
* 자리 자원을 **담는 곳**이다. 자원이 아니다
|
|
1296
|
+
* 로트 자재의 **개체**다. 자재 종류와 다른 층이다
|
|
1297
|
+
* 공정 자원을 **쓰는 일**이다
|
|
1298
|
+
*
|
|
1299
|
+
* 화면이 그 셋도 필요하면 다른 이름으로 든다. 이 목록은 「무엇이 자원인가」에만 답한다.
|
|
1300
|
+
*
|
|
1301
|
+
* ── `CapacityAxis` 와 다르다 ───────────────────────────────────────────────
|
|
1302
|
+
* 그것은 **제약이 걸린 축**이고(무엇을 늘려야 하는가), 자리가 들어간다 — 자리가 모자라도 못 돌기
|
|
1303
|
+
* 때문이다. 자재는 없다(늘린다고 지금 나오는 것이 아니다). 둘은 겹치지만 같지 않다.
|
|
1304
|
+
*
|
|
1305
|
+
* 합치면 한 값이 두 물음에 답하게 되고, 그러면 어느 쪽 뜻으로 읽어야 하는지 알 수 없다.
|
|
1306
|
+
*/
|
|
1307
|
+
export const RESOURCE_KIND = ['equipment', 'personnel', 'material', 'asset'];
|
|
1308
|
+
/** 선언된 자원 종류인가. */
|
|
1309
|
+
export function isResourceKind(value) {
|
|
1310
|
+
return typeof value === 'string' && RESOURCE_KIND.includes(value);
|
|
1311
|
+
}
|
|
1279
1312
|
/**
|
|
1280
1313
|
* 원본의 낱말을 우리 상태로 옮긴다 — **모르면 `undefined` 다.**
|
|
1281
1314
|
*
|
|
@@ -1318,6 +1351,31 @@ export function isFailureStatus(value) {
|
|
|
1318
1351
|
export function isPlannedStopStatus(value) {
|
|
1319
1352
|
return normalizeEquipmentStatus(value) === 'planned-stop';
|
|
1320
1353
|
}
|
|
1354
|
+
/**
|
|
1355
|
+
* 실제로 들어가고 나온 자재 한 줄 — ISA-95 `OpMaterialActualType` 의 부분집합.
|
|
1356
|
+
*
|
|
1357
|
+
* ── 왜 이름 있는 타입인가 (2026-08-31) ────────────────────────────────────
|
|
1358
|
+
* 같은 데이터가 네 곳에 각각 인라인으로 적혀 있었다(작업 상태 둘 · 커널 · ERP). ERP 쪽은 필드명까지
|
|
1359
|
+
* 달랐다(`materialDefinitionId` · `direction`). 같은 데이터를 여러 이름으로 부르면 옮기는 코드가
|
|
1360
|
+
* 생기고, 그 코드가 언젠가 어긋난다.
|
|
1361
|
+
*
|
|
1362
|
+
* 표준 매핑은 커널이 이미 적어 두었다 — `MaterialDefinitionID` → `definitionId`,
|
|
1363
|
+
* `MaterialUse` → `use`. 그 이름을 그대로 쓴다.
|
|
1364
|
+
*/
|
|
1365
|
+
/**
|
|
1366
|
+
* **실적의 자재 쓰임** — 표준 `MaterialUse`. 들어갔나 나왔나 둘이다.
|
|
1367
|
+
*
|
|
1368
|
+
* 공정 **명세**의 쓰임(`OperationMaterial.use`)은 셋이다 — 거기에는 `consumable` 이 있다(쓰이지만
|
|
1369
|
+
* 제품에 남지 않는 것: 세척수·윤활유). **실적에는 그 낱말이 없다** — 일어난 일을 적는 자리이고,
|
|
1370
|
+
* 소모품이 실제로 들어간 것은 `consumed` 다.
|
|
1371
|
+
*
|
|
1372
|
+
* 둘을 한 어휘로 묶지 않는다. 묶으면 실적에 `consumable` 이 올 수 있게 되고, 그때 그것이
|
|
1373
|
+
* 「들어갔다」인지 「쓰였지만 안 남았다」인지 받는 쪽이 정해야 한다.
|
|
1374
|
+
*
|
|
1375
|
+
* 유입 게이트가 이 상수를 가리킨다(§`operational-ingest.ts` 의 `shapes`) — 낱말을 두 곳에 적으면
|
|
1376
|
+
* 한쪽만 늘어나는 날 게이트가 계약에 없는 낱말을 통과시킨다.
|
|
1377
|
+
*/
|
|
1378
|
+
export const MATERIAL_ACTUAL_USE = ['consumed', 'produced'];
|
|
1321
1379
|
// ── Command 채널 어휘 — 트윈의 "행위(act)" 면 (prescriptive/트랜잭션 프론트엔드) ──
|
|
1322
1380
|
// 코어 공통: order.hold/resume(할당 보류). 도메인: order.release(즉시 투입) 등은 handleCommand 로.
|
|
1323
1381
|
export const CMD = {
|
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,6 +149,18 @@ 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`). 해소는 호스트가 한다. */
|
package/dist/epcis.d.ts
CHANGED
|
@@ -385,6 +385,24 @@ export declare function gdtiUri(companyPrefix: string, docType: string, serial:
|
|
|
385
385
|
* Link 를 **권한다**(SHOULD) — 이 길은 프리픽스가 없을 때의 정합 경로다.
|
|
386
386
|
*/
|
|
387
387
|
export declare function objectUri(namespace: string, objId: string | number): string | undefined;
|
|
388
|
+
/**
|
|
389
|
+
* **선언된 이름공간 아래의 클래스 식별자** — SGTIN·LGTIN 을 쓰지 않는 길.
|
|
390
|
+
*
|
|
391
|
+
* 품목 등급·로트 등급처럼 「낱개가 아니라 종류」를 가리키는 자리다. 개체 쪽(`objectUri`)과 같은
|
|
392
|
+
* 사정으로 필요하다: GTIN 을 조립하려면 회사 프리픽스가 있어야 하고, 없는 현장이 그 길로 못 간다.
|
|
393
|
+
*
|
|
394
|
+
* · CBV 2.0 §8.3.4 `http(s)://[Subdomain.]Domain/⁎⁎/class/ClassID`
|
|
395
|
+
* · CBV 2.0 §8.3.3 `urn:URNNamespace:⁎⁎:class:ClassID`
|
|
396
|
+
*
|
|
397
|
+
* ── 왜 계약에 두나 (2026-09-04, plant 레인이 올림) ─────────────────────────
|
|
398
|
+
* 체프 커넥터가 이 URI 를 **직접 조립하고 있었다** — `productClassId` · `lotClassId` 라는 자기
|
|
399
|
+
* 이름으로. 그중 `bizTransactionId` 는 이 파일의 `bizTransactionUri` 와 **같은 것을 다르게**
|
|
400
|
+
* 만들고 있었다. `underNamespace` 는 처음부터 `'class'` 표지를 알고 있었고(그 함수 주석에
|
|
401
|
+
* §8.3.3·§8.3.4 까지 적혀 있다) **부를 문만 없었다.**
|
|
402
|
+
*
|
|
403
|
+
* 그래서 한 표준에 방언이 둘이 됐다. 문을 낸다.
|
|
404
|
+
*/
|
|
405
|
+
export declare function classUri(namespace: string, classId: string | number): string | undefined;
|
|
388
406
|
export declare function bizTransactionUri(namespace: string, transId: string | number): string | undefined;
|
|
389
407
|
/** 모든 빌더가 공통으로 받는 표준 헤더 옵션(선택) — 방출부가 필요할 때 채운다. */
|
|
390
408
|
export interface EpcisHeaderOptions {
|
package/dist/epcis.js
CHANGED
|
@@ -263,6 +263,26 @@ export function gdtiUri(companyPrefix, docType, serial) {
|
|
|
263
263
|
export function objectUri(namespace, objId) {
|
|
264
264
|
return underNamespace(namespace, 'obj', objId);
|
|
265
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* **선언된 이름공간 아래의 클래스 식별자** — SGTIN·LGTIN 을 쓰지 않는 길.
|
|
268
|
+
*
|
|
269
|
+
* 품목 등급·로트 등급처럼 「낱개가 아니라 종류」를 가리키는 자리다. 개체 쪽(`objectUri`)과 같은
|
|
270
|
+
* 사정으로 필요하다: GTIN 을 조립하려면 회사 프리픽스가 있어야 하고, 없는 현장이 그 길로 못 간다.
|
|
271
|
+
*
|
|
272
|
+
* · CBV 2.0 §8.3.4 `http(s)://[Subdomain.]Domain/⁎⁎/class/ClassID`
|
|
273
|
+
* · CBV 2.0 §8.3.3 `urn:URNNamespace:⁎⁎:class:ClassID`
|
|
274
|
+
*
|
|
275
|
+
* ── 왜 계약에 두나 (2026-09-04, plant 레인이 올림) ─────────────────────────
|
|
276
|
+
* 체프 커넥터가 이 URI 를 **직접 조립하고 있었다** — `productClassId` · `lotClassId` 라는 자기
|
|
277
|
+
* 이름으로. 그중 `bizTransactionId` 는 이 파일의 `bizTransactionUri` 와 **같은 것을 다르게**
|
|
278
|
+
* 만들고 있었다. `underNamespace` 는 처음부터 `'class'` 표지를 알고 있었고(그 함수 주석에
|
|
279
|
+
* §8.3.3·§8.3.4 까지 적혀 있다) **부를 문만 없었다.**
|
|
280
|
+
*
|
|
281
|
+
* 그래서 한 표준에 방언이 둘이 됐다. 문을 낸다.
|
|
282
|
+
*/
|
|
283
|
+
export function classUri(namespace, classId) {
|
|
284
|
+
return underNamespace(namespace, 'class', classId);
|
|
285
|
+
}
|
|
266
286
|
export function bizTransactionUri(namespace, transId) {
|
|
267
287
|
return underNamespace(namespace, 'bt', transId);
|
|
268
288
|
}
|
|
@@ -271,15 +291,44 @@ export function bizTransactionUri(namespace, transId) {
|
|
|
271
291
|
*
|
|
272
292
|
* `obj`(개체 §8.2.3·§8.2.4) · `class`(클래스 §8.3.3·§8.3.4) · `bt`(거래문서 §8.5.4·§8.5.5) 가 같은
|
|
273
293
|
* 모양이다. 규칙을 세 곳에 적으면 한 곳만 고쳐지는 날이 온다.
|
|
294
|
+
*
|
|
295
|
+
* ── 구분자를 계약이 부호화한다 (2026-09-04) ────────────────────────────────
|
|
296
|
+
* 전에는 구분자가 들면 **아무것도 만들지 않았다.** 그러면 부르는 쪽이 자기 손으로 부호화해서
|
|
297
|
+
* 넘기게 되고 — 실제로 그랬다 — 부르는 쪽마다 부호화가 달라진다. 방금 `classUri` 가 없어서
|
|
298
|
+
* 방언이 생긴 것과 **같은 부류**다. 표지 규칙만 계약이 갖고 부호화는 소비처에 남기면 반쪽이다.
|
|
299
|
+
*
|
|
300
|
+
* 그래서 **원문을 받아 계약이 부호화한다.** 부르는 쪽은 미리 부호화하지 않는다 — 하면 두 번
|
|
301
|
+
* 부호화되어(`A%2FB` → `A%252FB`) 다른 개체가 된다.
|
|
302
|
+
*
|
|
303
|
+
* ── 저장된 식별자를 건드리지 않는다 ────────────────────────────────────────
|
|
304
|
+
* 이 함수가 내는 값은 **저널에 남는 정체성**이다(인티그레이션 레인 실측: 47만 건 넘음). 한 글자만
|
|
305
|
+
* 달라도 이전 사실과 새 사실이 다른 개체가 된다. 그래서 **구조를 깨는 글자만** 부호화한다.
|
|
306
|
+
*
|
|
307
|
+
* 부호화한다 % / : ? # 공백 URL 경로 성분이나 URN 성분 경계를 깬다(`%` 는 부호화의
|
|
308
|
+
* 부호이므로 먼저 해야 뜻이 하나로 정해진다)
|
|
309
|
+
* 그대로 둔다 그 밖의 모든 글자 영숫자 · `-` · `.` · `_` · `~` 를 포함해 전부
|
|
310
|
+
*
|
|
311
|
+
* 그래서 **위 여섯 글자가 없는 값은 바이트까지 그대로다** — 지금 저장된 값은 그 집합에 들었나만
|
|
312
|
+
* 확인하면 된다(그 확인은 값을 가진 쪽이 한다). 전에 통과하던 값 중 구분자가 든 것은 없었으므로
|
|
313
|
+
* (구분자가 들면 만들지 않았다) 이 변경으로 **바뀔 수 있는 것은 공백·`%`·`?`·`#` 이 든 값뿐**이다.
|
|
274
314
|
*/
|
|
315
|
+
const STRUCTURE_BREAKING = /[%/:?# ]/g;
|
|
316
|
+
const PERCENT_ENCODED = {
|
|
317
|
+
'%': '%25',
|
|
318
|
+
'/': '%2F',
|
|
319
|
+
':': '%3A',
|
|
320
|
+
'?': '%3F',
|
|
321
|
+
'#': '%23',
|
|
322
|
+
' ': '%20'
|
|
323
|
+
};
|
|
275
324
|
function underNamespace(namespace, marker, id) {
|
|
276
325
|
const ns = namespace?.trim();
|
|
277
326
|
if (!ns)
|
|
278
327
|
return undefined;
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
if (!v || v.includes('/') || v.includes(':'))
|
|
328
|
+
const raw = String(id);
|
|
329
|
+
if (!raw)
|
|
282
330
|
return undefined;
|
|
331
|
+
const v = raw.replace(STRUCTURE_BREAKING, c => PERCENT_ENCODED[c]);
|
|
283
332
|
if (/^https?:\/\/[^/\s]+/.test(ns))
|
|
284
333
|
return `${ns.replace(/\/+$/, '')}/${marker}/${v}`;
|
|
285
334
|
/*
|
|
@@ -447,7 +496,30 @@ const CBV_URN_CLASS = /^urn:[^:\s]+:(?:[^:\s]+:)*class:[^:\s]+$/;
|
|
|
447
496
|
* 합계만 본다 — 프리픽스가 몇 자리인지는 GS1 이 회사마다 다르게 배정하므로 우리가 알 수 없다.
|
|
448
497
|
* 합계는 키 종류가 정한다: SGTIN 13(GTIN-14 의 표시자+13자리) · SSCC 17 · GRAI 12 · GDTI 12.
|
|
449
498
|
*/
|
|
450
|
-
|
|
499
|
+
/*
|
|
500
|
+
* 키마다 「숫자 마디의 자릿수 합」 — 이 표에 없는 키는 아래 함수가 **아무 말도 하지 않는다.**
|
|
501
|
+
*
|
|
502
|
+
* ── lgtin 이 빠져 있었다 (2026-09-04, plant 레인 실측) ──────────────────────
|
|
503
|
+
* `urn:epc:class:lgtin:880.01.LOT-A` 는 합이 5 인데 그냥 지났고, 같은 값을 sgtin 으로 적으면
|
|
504
|
+
* 거절됐다. **하필 로트로 관리하는 자재가 쓰는 종류다** — 원자재·화학·식품이 다 여기다.
|
|
505
|
+
*
|
|
506
|
+
* 규칙은 이미 이 파일의 `lgtinClass` 주석에 적혀 있었다("두 숫자 마디의 자릿수 합은 13", EPC TDS
|
|
507
|
+
* 2.1.0 §6.4.1). 규칙을 알고, 검사도 있고, **표에 키만 없었다.**
|
|
508
|
+
*
|
|
509
|
+
* 셋째 마디(로트)는 숫자가 아니다 — 아래 함수가 앞의 두 마디만 세므로 그대로 맞다.
|
|
510
|
+
*
|
|
511
|
+
* ── sgln 도 같이 없었다 ────────────────────────────────────────────────────
|
|
512
|
+
* 로트를 찾다가 봤다. GLN 은 검사숫자를 뺀 두 마디 합이 12 라 `grai`·`gdti` 와 같은 규칙인데
|
|
513
|
+
* 표에 없었다.
|
|
514
|
+
*
|
|
515
|
+
* 값이 흐르고 있는지는 재봤다: **아직 안 흐른다.** 접수면에 자리가 있고(`RefLocation.gs1Id`)
|
|
516
|
+
* 매핑도 그것을 옮기는데, plant 마스터의 자리 표에는 그 컬럼 자체가 없다(`properties` 안에도
|
|
517
|
+
* 없다 — 0건). 그러니 이 키를 더해서 **지금 거절되는 값은 없다.** 오는 날에 맞아 있으려고 둔다.
|
|
518
|
+
*
|
|
519
|
+
* `giai`(개별 자산)는 일부러 없다 — 자산 참조 길이가 가변이어서 **합이 고정되지 않는다**(전체
|
|
520
|
+
* 30자리 이내라는 상한만 있다). 자릿수로 판정할 수 없는 키를 표에 넣으면 맞는 값을 거절한다.
|
|
521
|
+
*/
|
|
522
|
+
const GS1_KEY_DIGITS = { sgtin: 13, lgtin: 13, sscc: 17, grai: 12, gdti: 12, sgln: 12 };
|
|
451
523
|
/**
|
|
452
524
|
* 그 GS1 키의 자리 수가 맞나 — 어긋나면 왜인지 말한다.
|
|
453
525
|
*
|
|
@@ -19,6 +19,34 @@ export interface OperationalIngestOptions {
|
|
|
19
19
|
/** 레코드에 시각이 없을 때 쓸 값 — 주지 않으면 그 레코드를 거부한다. */
|
|
20
20
|
defaultEventTime?: string;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* **객체 안쪽의 모양** — `object`·`object[]` 자리가 계약의 타입을 실제로 보게 한다.
|
|
24
|
+
*
|
|
25
|
+
* ── 왜 필요한가 (2026-09-04 실측, 인티그레이션 레인이 찾음) ────────────────
|
|
26
|
+
* `materialActual: 'object[]'` 은 **객체 배열이면 지나갔다.** 그래서 틀린 이름이 저널에 앉았다.
|
|
27
|
+
*
|
|
28
|
+
* 계약의 이름 definitionId
|
|
29
|
+
* 온 것 materialDefinitionId
|
|
30
|
+
*
|
|
31
|
+
* 적히나 예 — 저널에 그 줄이 그대로 있다
|
|
32
|
+
* 읽히나 아니오 — 계약의 타입으로 읽으면 undefined 다
|
|
33
|
+
*
|
|
34
|
+
* 거절도 경고도 없었다. **같은 계약의 ERP 경로는 이것을 잡는다**(§`erp.ts` 의 자재 줄 검사).
|
|
35
|
+
* 한 계약 안에 검사가 두 벌이고 한쪽만 봤다.
|
|
36
|
+
*
|
|
37
|
+
* 이름을 맞춰 달라고 원본에 청하는 것으로는 다음에 또 조용해진다 — 다음 원본이 `use: 'out'` 같은
|
|
38
|
+
* 것을 보내면 같은 일이 난다(실제로 그런 시드가 있었다: 계약에 `out` 이라는 낱말이 없다).
|
|
39
|
+
*/
|
|
40
|
+
export interface ObjectShape {
|
|
41
|
+
/** 반드시 있어야 하는 안쪽 필드 — 비어 있으면 그 줄을 거절한다. */
|
|
42
|
+
required?: readonly string[];
|
|
43
|
+
/** 안쪽 필드의 형 — 선언한 것만 본다. */
|
|
44
|
+
fields?: Readonly<Record<string, 'string' | 'number' | 'boolean'>>;
|
|
45
|
+
/** 낱말이 정해진 안쪽 필드 — 계약에 없는 낱말을 거절한다. */
|
|
46
|
+
enums?: Readonly<Record<string, readonly string[]>>;
|
|
47
|
+
/** 0 보다 커야 하는 수 — 「모른다」는 **비워서** 말한다(0 으로 말하지 않는다). */
|
|
48
|
+
positive?: readonly string[];
|
|
49
|
+
}
|
|
22
50
|
/**
|
|
23
51
|
* 이 레코드가 어느 운영 사실인가 — **라우팅 판정을 한 곳에 둔다**(소비처가 각자 짐작하지 않게).
|
|
24
52
|
*
|
|
@@ -34,7 +34,59 @@
|
|
|
34
34
|
* 모르는 필드는 **조용히 버리지 않고 거부한다.** `taskID` 처럼 한 글자 틀린 이름은 통과시키면 영원히
|
|
35
35
|
* 보이지 않는 손실이 된다(이 문에는 아직 옛 발신자가 없어 호환 부담도 없다).
|
|
36
36
|
*/
|
|
37
|
-
import { OP_EVENT, DISPOSITION_DECISION } from "./contract.js";
|
|
37
|
+
import { OP_EVENT, DISPOSITION_DECISION, MATERIAL_ACTUAL_USE } from "./contract.js";
|
|
38
|
+
/**
|
|
39
|
+
* 객체 한 줄이 선언된 안쪽 모양을 지키나 — **선언하지 않은 자리는 보지 않는다.**
|
|
40
|
+
*
|
|
41
|
+
* 순수하다. 그래서 「어떤 줄이 거절되나」를 값만 바꿔 가며 잴 수 있다.
|
|
42
|
+
*
|
|
43
|
+
* ── 「모른다」를 0 으로 말하지 않는다 ───────────────────────────────────────
|
|
44
|
+
* `positive` 에 든 수는 0 보다 커야 한다. 라벨에 수량이 없는 것이 정상인 현장이 있는데(한 통·한
|
|
45
|
+
* 팔레트), 그때 **비워서** 말한다 — 0 으로 실으면 받는 쪽에서 「0개」와 「모른다」가 같아지고,
|
|
46
|
+
* 소요량 집계가 조용히 틀린다.
|
|
47
|
+
*
|
|
48
|
+
* 없는 필드는 `required` 가 아니면 그냥 없는 것이다. 이 함수가 채워 주지 않는다.
|
|
49
|
+
*/
|
|
50
|
+
function shapeErrors(where, row, shape) {
|
|
51
|
+
if (!shape)
|
|
52
|
+
return [];
|
|
53
|
+
const errors = [];
|
|
54
|
+
for (const f of shape.required ?? []) {
|
|
55
|
+
const v = row[f];
|
|
56
|
+
const empty = v === undefined || v === null || (typeof v === 'string' && !v.trim());
|
|
57
|
+
if (empty)
|
|
58
|
+
errors.push(`${where}.${f} 가 없다 — 계약이 요구하는 이름이다`);
|
|
59
|
+
}
|
|
60
|
+
for (const [f, t] of Object.entries(shape.fields ?? {})) {
|
|
61
|
+
const v = row[f];
|
|
62
|
+
if (v === undefined || v === null)
|
|
63
|
+
continue;
|
|
64
|
+
if (t === 'number' && (typeof v !== 'number' || !Number.isFinite(v))) {
|
|
65
|
+
errors.push(`${where}.${f} 가 수가 아니다: ${JSON.stringify(v)}`);
|
|
66
|
+
}
|
|
67
|
+
else if (t !== 'number' && typeof v !== t) {
|
|
68
|
+
errors.push(`${where}.${f} 가 ${t} 가 아니다: ${JSON.stringify(v)}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
for (const [f, allowed] of Object.entries(shape.enums ?? {})) {
|
|
72
|
+
const v = row[f];
|
|
73
|
+
if (v === undefined || v === null)
|
|
74
|
+
continue;
|
|
75
|
+
if (!allowed.includes(String(v))) {
|
|
76
|
+
errors.push(`${where}.${f} 에 계약에 없는 낱말이 왔다: ${JSON.stringify(v)} (${allowed.join(' · ')})`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
for (const f of shape.positive ?? []) {
|
|
80
|
+
const v = row[f];
|
|
81
|
+
/* 없는 것은 「모른다」다 — 여기서 막지 않는다. 위 `required` 가 필요하면 거기서 막는다. */
|
|
82
|
+
if (v === undefined || v === null)
|
|
83
|
+
continue;
|
|
84
|
+
if (typeof v !== 'number' || !(v > 0)) {
|
|
85
|
+
errors.push(`${where}.${f} 가 0 이하다: ${JSON.stringify(v)} — 모르는 수량은 0 이 아니라 비워서 말한다`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return errors;
|
|
89
|
+
}
|
|
38
90
|
/**
|
|
39
91
|
* 닫아 둔 낱말과 그 이유.
|
|
40
92
|
* · 작업 상태 — 성과 폴드가 `completed`·`in-progress` 로 갈린다(`kpi-fold`).
|
|
@@ -72,6 +124,27 @@ const SPECS = {
|
|
|
72
124
|
startedAtSimMs: 'number', outcome: 'string', priority: 'number', startTime: 'string', endTime: 'string',
|
|
73
125
|
materialActual: 'object[]', recordTime: 'string'
|
|
74
126
|
},
|
|
127
|
+
/*
|
|
128
|
+
* **자재 줄의 안쪽** — 계약의 `MaterialActual` 그대로다.
|
|
129
|
+
*
|
|
130
|
+
* `definitionId` 를 요구하는 이유: 이 이름이 없으면 그 줄이 무엇을 가리키는지 아무도 모른다.
|
|
131
|
+
* 실제로 `materialDefinitionId` 로 온 줄이 저널에 앉았고, 계약의 타입으로 읽으면 비어 있었다.
|
|
132
|
+
*
|
|
133
|
+
* `quantity` 를 `positive` 에 두는 이유: 「모른다」는 **비워서** 말한다. 0 으로 실으면 받는
|
|
134
|
+
* 쪽에서 「0개」와 구별되지 않고, 소요량 집계가 조용히 틀린다.
|
|
135
|
+
*
|
|
136
|
+
* `uom` 은 요구하지 않는다 — 계약에서 선택이다(§`MaterialActual`). ERP 경로는 요구하는데
|
|
137
|
+
* 그것은 **그 경로의 규칙**이다(정산에 단위 없는 수량을 쓸 수 없다). 커널이 그 규칙을 통째로
|
|
138
|
+
* 물려받으면 단위를 모르는 현장의 사실이 통째로 거절된다.
|
|
139
|
+
*/
|
|
140
|
+
shapes: {
|
|
141
|
+
materialActual: {
|
|
142
|
+
required: ['definitionId', 'use'],
|
|
143
|
+
fields: { definitionId: 'string', lotId: 'string', quantity: 'number', uom: 'string' },
|
|
144
|
+
enums: { use: MATERIAL_ACTUAL_USE },
|
|
145
|
+
positive: ['quantity']
|
|
146
|
+
}
|
|
147
|
+
},
|
|
75
148
|
enums: {
|
|
76
149
|
status: TASK_STATUS,
|
|
77
150
|
intent: ['transport', 'process', 'dwell'],
|
|
@@ -428,6 +501,7 @@ export function ingestOperationalRecords(records, opts) {
|
|
|
428
501
|
errors.push(`${kind}.${name} 가 객체가 아니다: ${JSON.stringify(v)}`);
|
|
429
502
|
break;
|
|
430
503
|
}
|
|
504
|
+
errors.push(...shapeErrors(`${kind}.${name}`, v, spec.shapes?.[name]));
|
|
431
505
|
data[name] = { ...v };
|
|
432
506
|
break;
|
|
433
507
|
}
|
|
@@ -436,6 +510,7 @@ export function ingestOperationalRecords(records, opts) {
|
|
|
436
510
|
errors.push(`${kind}.${name} 가 객체 배열이 아니다: ${JSON.stringify(v)}`);
|
|
437
511
|
break;
|
|
438
512
|
}
|
|
513
|
+
v.forEach((x, i) => errors.push(...shapeErrors(`${kind}.${name}[${i}]`, x, spec.shapes?.[name])));
|
|
439
514
|
data[name] = v.map(x => ({ ...x }));
|
|
440
515
|
break;
|
|
441
516
|
}
|
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
|
@@ -50,6 +50,7 @@ __export(index_exports, {
|
|
|
50
50
|
GUARD_PRAGMA: () => GUARD_PRAGMA,
|
|
51
51
|
ILMD_ATTR: () => ILMD_ATTR,
|
|
52
52
|
LOCATION_SATURATION_NEAR: () => LOCATION_SATURATION_NEAR,
|
|
53
|
+
MATERIAL_ACTUAL_USE: () => MATERIAL_ACTUAL_USE,
|
|
53
54
|
MATERIAL_PROPERTY: () => MATERIAL_PROPERTY,
|
|
54
55
|
MES_BIZSTEP: () => MES_BIZSTEP,
|
|
55
56
|
MES_COMMANDS: () => MES_COMMANDS,
|
|
@@ -61,6 +62,7 @@ __export(index_exports, {
|
|
|
61
62
|
OP_PARAM: () => OP_PARAM,
|
|
62
63
|
ORDER_TERMINAL_STATUS: () => ORDER_TERMINAL_STATUS,
|
|
63
64
|
PRIORITY_UNSET: () => PRIORITY_UNSET,
|
|
65
|
+
RESOURCE_KIND: () => RESOURCE_KIND,
|
|
64
66
|
RETIRED_VOCABULARY: () => RETIRED_VOCABULARY,
|
|
65
67
|
SCHEDULE_STATUS: () => SCHEDULE_STATUS,
|
|
66
68
|
TWIN_AXES: () => TWIN_AXES,
|
|
@@ -95,12 +97,14 @@ __export(index_exports, {
|
|
|
95
97
|
checkSequenceRun: () => checkSequenceRun,
|
|
96
98
|
classClosure: () => classClosure,
|
|
97
99
|
classIdentifierViolation: () => classIdentifierViolation,
|
|
100
|
+
classUri: () => classUri,
|
|
98
101
|
commandFromSpec: () => commandFromSpec,
|
|
99
102
|
commandSpecGaps: () => commandSpecGaps,
|
|
100
103
|
commandTypeOf: () => commandTypeOf,
|
|
101
104
|
commandsOf: () => commandsOf,
|
|
102
105
|
computeFirstPassYield: () => computeFirstPassYield,
|
|
103
106
|
computeMtbf: () => computeMtbf,
|
|
107
|
+
computeMttr: () => computeMttr,
|
|
104
108
|
computeOee: () => computeOee,
|
|
105
109
|
conversionFactorOf: () => conversionFactorOf,
|
|
106
110
|
criterionSaysNothing: () => criterionSaysNothing,
|
|
@@ -149,6 +153,7 @@ __export(index_exports, {
|
|
|
149
153
|
isOperationalRecord: () => isOperationalRecord,
|
|
150
154
|
isOrderTerminal: () => isOrderTerminal,
|
|
151
155
|
isPlannedStopStatus: () => isPlannedStopStatus,
|
|
156
|
+
isResourceKind: () => isResourceKind,
|
|
152
157
|
isTransformationRecord: () => isTransformationRecord,
|
|
153
158
|
isoDurationHours: () => isoDurationHours,
|
|
154
159
|
itemKeyOf: () => itemKeyOf,
|
|
@@ -201,6 +206,7 @@ __export(index_exports, {
|
|
|
201
206
|
ssccUri: () => ssccUri,
|
|
202
207
|
stateFieldsOf: () => stateFieldsOf,
|
|
203
208
|
subLotIdOf: () => subLotIdOf,
|
|
209
|
+
tenseOf: () => tenseOf,
|
|
204
210
|
testEvidenceGaps: () => testEvidenceGaps,
|
|
205
211
|
testPassedAt: () => testPassedAt,
|
|
206
212
|
transactionEvent: () => transactionEvent,
|
|
@@ -620,8 +626,8 @@ function dueStatusOf(x, nowIso) {
|
|
|
620
626
|
if (!Number.isFinite(due) || !Number.isFinite(now)) return void 0;
|
|
621
627
|
return now > due ? "late" : "on-time";
|
|
622
628
|
}
|
|
623
|
-
function subLotIdOf(
|
|
624
|
-
return `${
|
|
629
|
+
function subLotIdOf(classUri2, location) {
|
|
630
|
+
return `${classUri2}@${location}`;
|
|
625
631
|
}
|
|
626
632
|
function itemKeyOf(item) {
|
|
627
633
|
return item.subLotId ?? item.epc;
|
|
@@ -1022,6 +1028,10 @@ var EQUIPMENT_STATUS = ["busy", "setup", "down", "idle", "planned-stop"];
|
|
|
1022
1028
|
function isEquipmentStatus(value) {
|
|
1023
1029
|
return typeof value === "string" && EQUIPMENT_STATUS.includes(value);
|
|
1024
1030
|
}
|
|
1031
|
+
var RESOURCE_KIND = ["equipment", "personnel", "material", "asset"];
|
|
1032
|
+
function isResourceKind(value) {
|
|
1033
|
+
return typeof value === "string" && RESOURCE_KIND.includes(value);
|
|
1034
|
+
}
|
|
1025
1035
|
function normalizeEquipmentStatus(value) {
|
|
1026
1036
|
const said = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
1027
1037
|
return isEquipmentStatus(said) ? said : void 0;
|
|
@@ -1032,6 +1042,7 @@ function isFailureStatus(value) {
|
|
|
1032
1042
|
function isPlannedStopStatus(value) {
|
|
1033
1043
|
return normalizeEquipmentStatus(value) === "planned-stop";
|
|
1034
1044
|
}
|
|
1045
|
+
var MATERIAL_ACTUAL_USE = ["consumed", "produced"];
|
|
1035
1046
|
var CMD = {
|
|
1036
1047
|
orderHold: "order.hold",
|
|
1037
1048
|
orderResume: "order.resume",
|
|
@@ -1857,6 +1868,9 @@ var TWIN_AXES = [
|
|
|
1857
1868
|
systems: ["ems"]
|
|
1858
1869
|
}
|
|
1859
1870
|
];
|
|
1871
|
+
function tenseOf(r) {
|
|
1872
|
+
return r?.tense;
|
|
1873
|
+
}
|
|
1860
1874
|
var TWIN_RELATIONS = [
|
|
1861
1875
|
/* 자리가 속한 구역 — **board 밖**(호스트의 `TwinArea`). 해소는 호스트가 한다. */
|
|
1862
1876
|
{ from: "locations", field: "parentId", target: { kind: "external", entity: "space.area" }, via: "twin.rel.area", optional: true },
|
|
@@ -2791,14 +2805,27 @@ function gdtiUri(companyPrefix, docType, serial) {
|
|
|
2791
2805
|
function objectUri(namespace, objId) {
|
|
2792
2806
|
return underNamespace(namespace, "obj", objId);
|
|
2793
2807
|
}
|
|
2808
|
+
function classUri(namespace, classId) {
|
|
2809
|
+
return underNamespace(namespace, "class", classId);
|
|
2810
|
+
}
|
|
2794
2811
|
function bizTransactionUri(namespace, transId) {
|
|
2795
2812
|
return underNamespace(namespace, "bt", transId);
|
|
2796
2813
|
}
|
|
2814
|
+
var STRUCTURE_BREAKING = /[%/:?# ]/g;
|
|
2815
|
+
var PERCENT_ENCODED = {
|
|
2816
|
+
"%": "%25",
|
|
2817
|
+
"/": "%2F",
|
|
2818
|
+
":": "%3A",
|
|
2819
|
+
"?": "%3F",
|
|
2820
|
+
"#": "%23",
|
|
2821
|
+
" ": "%20"
|
|
2822
|
+
};
|
|
2797
2823
|
function underNamespace(namespace, marker, id) {
|
|
2798
2824
|
const ns = namespace?.trim();
|
|
2799
2825
|
if (!ns) return void 0;
|
|
2800
|
-
const
|
|
2801
|
-
if (!
|
|
2826
|
+
const raw = String(id);
|
|
2827
|
+
if (!raw) return void 0;
|
|
2828
|
+
const v = raw.replace(STRUCTURE_BREAKING, (c) => PERCENT_ENCODED[c]);
|
|
2802
2829
|
if (/^https?:\/\/[^/\s]+/.test(ns)) return `${ns.replace(/\/+$/, "")}/${marker}/${v}`;
|
|
2803
2830
|
if (/^urn:epc(global)?:/.test(ns)) return void 0;
|
|
2804
2831
|
if (/^urn:[^:\s]+/.test(ns)) return `${ns.replace(/:+$/, "")}:${marker}:${v}`;
|
|
@@ -2877,7 +2904,7 @@ var EPC_CLASS_PREFIXES = ["urn:epc:idpat:", "urn:epc:class:"];
|
|
|
2877
2904
|
var DL_CANONICAL = "https://id.gs1.org/";
|
|
2878
2905
|
var CBV_URL_CLASS = /^https?:\/\/[^/\s]+\/(?:[^/\s]+\/)*class\/[^/\s]+$/;
|
|
2879
2906
|
var CBV_URN_CLASS = /^urn:[^:\s]+:(?:[^:\s]+:)*class:[^:\s]+$/;
|
|
2880
|
-
var GS1_KEY_DIGITS = { sgtin: 13, sscc: 17, grai: 12, gdti: 12 };
|
|
2907
|
+
var GS1_KEY_DIGITS = { sgtin: 13, lgtin: 13, sscc: 17, grai: 12, gdti: 12, sgln: 12 };
|
|
2881
2908
|
function gs1KeyDigitViolation(uri) {
|
|
2882
2909
|
if (!uri) return void 0;
|
|
2883
2910
|
const m = /^urn:epc:(?:id|idpat|class):([a-z]+):([^:]+)$/.exec(uri);
|
|
@@ -3257,6 +3284,39 @@ function readEpochMs(raw) {
|
|
|
3257
3284
|
}
|
|
3258
3285
|
|
|
3259
3286
|
// src/operational-ingest.ts
|
|
3287
|
+
function shapeErrors(where, row, shape) {
|
|
3288
|
+
if (!shape) return [];
|
|
3289
|
+
const errors = [];
|
|
3290
|
+
for (const f of shape.required ?? []) {
|
|
3291
|
+
const v = row[f];
|
|
3292
|
+
const empty = v === void 0 || v === null || typeof v === "string" && !v.trim();
|
|
3293
|
+
if (empty) errors.push(`${where}.${f} \uAC00 \uC5C6\uB2E4 \u2014 \uACC4\uC57D\uC774 \uC694\uAD6C\uD558\uB294 \uC774\uB984\uC774\uB2E4`);
|
|
3294
|
+
}
|
|
3295
|
+
for (const [f, t] of Object.entries(shape.fields ?? {})) {
|
|
3296
|
+
const v = row[f];
|
|
3297
|
+
if (v === void 0 || v === null) continue;
|
|
3298
|
+
if (t === "number" && (typeof v !== "number" || !Number.isFinite(v))) {
|
|
3299
|
+
errors.push(`${where}.${f} \uAC00 \uC218\uAC00 \uC544\uB2C8\uB2E4: ${JSON.stringify(v)}`);
|
|
3300
|
+
} else if (t !== "number" && typeof v !== t) {
|
|
3301
|
+
errors.push(`${where}.${f} \uAC00 ${t} \uAC00 \uC544\uB2C8\uB2E4: ${JSON.stringify(v)}`);
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
for (const [f, allowed] of Object.entries(shape.enums ?? {})) {
|
|
3305
|
+
const v = row[f];
|
|
3306
|
+
if (v === void 0 || v === null) continue;
|
|
3307
|
+
if (!allowed.includes(String(v))) {
|
|
3308
|
+
errors.push(`${where}.${f} \uC5D0 \uACC4\uC57D\uC5D0 \uC5C6\uB294 \uB0B1\uB9D0\uC774 \uC654\uB2E4: ${JSON.stringify(v)} (${allowed.join(" \xB7 ")})`);
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
for (const f of shape.positive ?? []) {
|
|
3312
|
+
const v = row[f];
|
|
3313
|
+
if (v === void 0 || v === null) continue;
|
|
3314
|
+
if (typeof v !== "number" || !(v > 0)) {
|
|
3315
|
+
errors.push(`${where}.${f} \uAC00 0 \uC774\uD558\uB2E4: ${JSON.stringify(v)} \u2014 \uBAA8\uB974\uB294 \uC218\uB7C9\uC740 0 \uC774 \uC544\uB2C8\uB77C \uBE44\uC6CC\uC11C \uB9D0\uD55C\uB2E4`);
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
return errors;
|
|
3319
|
+
}
|
|
3260
3320
|
var TASK_STATUS = ["created", "assigned", "in-progress", "completed"];
|
|
3261
3321
|
var EQUIPMENT_STATUS2 = ["idle", "busy", "down", "setup", "planned-stop"];
|
|
3262
3322
|
var PERSON_STATUS = ["idle", "busy"];
|
|
@@ -3293,6 +3353,27 @@ var SPECS = {
|
|
|
3293
3353
|
materialActual: "object[]",
|
|
3294
3354
|
recordTime: "string"
|
|
3295
3355
|
},
|
|
3356
|
+
/*
|
|
3357
|
+
* **자재 줄의 안쪽** — 계약의 `MaterialActual` 그대로다.
|
|
3358
|
+
*
|
|
3359
|
+
* `definitionId` 를 요구하는 이유: 이 이름이 없으면 그 줄이 무엇을 가리키는지 아무도 모른다.
|
|
3360
|
+
* 실제로 `materialDefinitionId` 로 온 줄이 저널에 앉았고, 계약의 타입으로 읽으면 비어 있었다.
|
|
3361
|
+
*
|
|
3362
|
+
* `quantity` 를 `positive` 에 두는 이유: 「모른다」는 **비워서** 말한다. 0 으로 실으면 받는
|
|
3363
|
+
* 쪽에서 「0개」와 구별되지 않고, 소요량 집계가 조용히 틀린다.
|
|
3364
|
+
*
|
|
3365
|
+
* `uom` 은 요구하지 않는다 — 계약에서 선택이다(§`MaterialActual`). ERP 경로는 요구하는데
|
|
3366
|
+
* 그것은 **그 경로의 규칙**이다(정산에 단위 없는 수량을 쓸 수 없다). 커널이 그 규칙을 통째로
|
|
3367
|
+
* 물려받으면 단위를 모르는 현장의 사실이 통째로 거절된다.
|
|
3368
|
+
*/
|
|
3369
|
+
shapes: {
|
|
3370
|
+
materialActual: {
|
|
3371
|
+
required: ["definitionId", "use"],
|
|
3372
|
+
fields: { definitionId: "string", lotId: "string", quantity: "number", uom: "string" },
|
|
3373
|
+
enums: { use: MATERIAL_ACTUAL_USE },
|
|
3374
|
+
positive: ["quantity"]
|
|
3375
|
+
}
|
|
3376
|
+
},
|
|
3296
3377
|
enums: {
|
|
3297
3378
|
status: TASK_STATUS,
|
|
3298
3379
|
intent: ["transport", "process", "dwell"],
|
|
@@ -3675,6 +3756,7 @@ function ingestOperationalRecords(records, opts) {
|
|
|
3675
3756
|
errors.push(`${kind}.${name} \uAC00 \uAC1D\uCCB4\uAC00 \uC544\uB2C8\uB2E4: ${JSON.stringify(v)}`);
|
|
3676
3757
|
break;
|
|
3677
3758
|
}
|
|
3759
|
+
errors.push(...shapeErrors(`${kind}.${name}`, v, spec.shapes?.[name]));
|
|
3678
3760
|
data[name] = { ...v };
|
|
3679
3761
|
break;
|
|
3680
3762
|
}
|
|
@@ -3683,6 +3765,9 @@ function ingestOperationalRecords(records, opts) {
|
|
|
3683
3765
|
errors.push(`${kind}.${name} \uAC00 \uAC1D\uCCB4 \uBC30\uC5F4\uC774 \uC544\uB2C8\uB2E4: ${JSON.stringify(v)}`);
|
|
3684
3766
|
break;
|
|
3685
3767
|
}
|
|
3768
|
+
v.forEach(
|
|
3769
|
+
(x, i) => errors.push(...shapeErrors(`${kind}.${name}[${i}]`, x, spec.shapes?.[name]))
|
|
3770
|
+
);
|
|
3686
3771
|
data[name] = v.map((x) => ({ ...x }));
|
|
3687
3772
|
break;
|
|
3688
3773
|
}
|
|
@@ -3996,12 +4081,11 @@ function rolledThroughputYield(steps) {
|
|
|
3996
4081
|
|
|
3997
4082
|
// src/reliability.ts
|
|
3998
4083
|
var at = (t) => typeof t === "string" ? Date.parse(t) : NaN;
|
|
3999
|
-
function
|
|
4000
|
-
const { equipmentId } = input;
|
|
4001
|
-
const missing = [];
|
|
4084
|
+
function scanPeriods(periods) {
|
|
4002
4085
|
const unknown = /* @__PURE__ */ new Set();
|
|
4003
|
-
const sorted = [...
|
|
4086
|
+
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);
|
|
4004
4087
|
let operatingMs = 0;
|
|
4088
|
+
let downMs = 0;
|
|
4005
4089
|
let failures = 0;
|
|
4006
4090
|
let downPeriods = 0;
|
|
4007
4091
|
let invalid = false;
|
|
@@ -4018,6 +4102,7 @@ function computeMtbf(input) {
|
|
|
4018
4102
|
}
|
|
4019
4103
|
if (p.status === "busy") operatingMs += p.to - p.from;
|
|
4020
4104
|
if (p.status === "down") {
|
|
4105
|
+
downMs += p.to - p.from;
|
|
4021
4106
|
downPeriods++;
|
|
4022
4107
|
if (!wasDown) failures++;
|
|
4023
4108
|
wasDown = true;
|
|
@@ -4025,20 +4110,42 @@ function computeMtbf(input) {
|
|
|
4025
4110
|
wasDown = false;
|
|
4026
4111
|
}
|
|
4027
4112
|
}
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4113
|
+
return { operatingMs, downMs, failures, downPeriods, unknownStatuses: [...unknown], invalid };
|
|
4114
|
+
}
|
|
4115
|
+
function computeMtbf(input) {
|
|
4116
|
+
const s = scanPeriods(input.periods);
|
|
4117
|
+
const missing = [];
|
|
4118
|
+
if (s.invalid) missing.push("invalid-period");
|
|
4119
|
+
if (s.unknownStatuses.length) missing.push("unknown-status");
|
|
4120
|
+
if (s.operatingMs <= 0) missing.push("no-operating-time");
|
|
4121
|
+
if (s.failures === 0) missing.push("no-failure");
|
|
4122
|
+
const out = {
|
|
4123
|
+
equipmentId: input.equipmentId,
|
|
4124
|
+
operatingMs: s.operatingMs,
|
|
4125
|
+
failures: s.failures,
|
|
4126
|
+
downPeriods: s.downPeriods,
|
|
4127
|
+
unknownStatuses: s.unknownStatuses,
|
|
4128
|
+
missing
|
|
4129
|
+
};
|
|
4130
|
+
if (missing.length) return out;
|
|
4131
|
+
return { ...out, mtbfMs: s.operatingMs / s.failures };
|
|
4132
|
+
}
|
|
4133
|
+
function computeMttr(input) {
|
|
4134
|
+
const s = scanPeriods(input.periods);
|
|
4135
|
+
const missing = [];
|
|
4136
|
+
if (s.invalid) missing.push("invalid-period");
|
|
4137
|
+
if (s.unknownStatuses.length) missing.push("unknown-status");
|
|
4138
|
+
if (s.failures === 0) missing.push("no-failure");
|
|
4032
4139
|
const out = {
|
|
4033
|
-
equipmentId,
|
|
4034
|
-
|
|
4035
|
-
failures,
|
|
4036
|
-
downPeriods,
|
|
4037
|
-
unknownStatuses:
|
|
4140
|
+
equipmentId: input.equipmentId,
|
|
4141
|
+
downMs: s.downMs,
|
|
4142
|
+
failures: s.failures,
|
|
4143
|
+
downPeriods: s.downPeriods,
|
|
4144
|
+
unknownStatuses: s.unknownStatuses,
|
|
4038
4145
|
missing
|
|
4039
4146
|
};
|
|
4040
4147
|
if (missing.length) return out;
|
|
4041
|
-
return { ...out,
|
|
4148
|
+
return { ...out, mttrMs: s.downMs / s.failures };
|
|
4042
4149
|
}
|
|
4043
4150
|
|
|
4044
4151
|
// src/erp.ts
|
|
@@ -4233,6 +4340,7 @@ function commandSpecGaps(specs, command) {
|
|
|
4233
4340
|
GUARD_PRAGMA,
|
|
4234
4341
|
ILMD_ATTR,
|
|
4235
4342
|
LOCATION_SATURATION_NEAR,
|
|
4343
|
+
MATERIAL_ACTUAL_USE,
|
|
4236
4344
|
MATERIAL_PROPERTY,
|
|
4237
4345
|
MES_BIZSTEP,
|
|
4238
4346
|
MES_COMMANDS,
|
|
@@ -4244,6 +4352,7 @@ function commandSpecGaps(specs, command) {
|
|
|
4244
4352
|
OP_PARAM,
|
|
4245
4353
|
ORDER_TERMINAL_STATUS,
|
|
4246
4354
|
PRIORITY_UNSET,
|
|
4355
|
+
RESOURCE_KIND,
|
|
4247
4356
|
RETIRED_VOCABULARY,
|
|
4248
4357
|
SCHEDULE_STATUS,
|
|
4249
4358
|
TWIN_AXES,
|
|
@@ -4278,12 +4387,14 @@ function commandSpecGaps(specs, command) {
|
|
|
4278
4387
|
checkSequenceRun,
|
|
4279
4388
|
classClosure,
|
|
4280
4389
|
classIdentifierViolation,
|
|
4390
|
+
classUri,
|
|
4281
4391
|
commandFromSpec,
|
|
4282
4392
|
commandSpecGaps,
|
|
4283
4393
|
commandTypeOf,
|
|
4284
4394
|
commandsOf,
|
|
4285
4395
|
computeFirstPassYield,
|
|
4286
4396
|
computeMtbf,
|
|
4397
|
+
computeMttr,
|
|
4287
4398
|
computeOee,
|
|
4288
4399
|
conversionFactorOf,
|
|
4289
4400
|
criterionSaysNothing,
|
|
@@ -4332,6 +4443,7 @@ function commandSpecGaps(specs, command) {
|
|
|
4332
4443
|
isOperationalRecord,
|
|
4333
4444
|
isOrderTerminal,
|
|
4334
4445
|
isPlannedStopStatus,
|
|
4446
|
+
isResourceKind,
|
|
4335
4447
|
isTransformationRecord,
|
|
4336
4448
|
isoDurationHours,
|
|
4337
4449
|
itemKeyOf,
|
|
@@ -4384,6 +4496,7 @@ function commandSpecGaps(specs, command) {
|
|
|
4384
4496
|
ssccUri,
|
|
4385
4497
|
stateFieldsOf,
|
|
4386
4498
|
subLotIdOf,
|
|
4499
|
+
tenseOf,
|
|
4387
4500
|
testEvidenceGaps,
|
|
4388
4501
|
testPassedAt,
|
|
4389
4502
|
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.5",
|
|
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",
|