@operato/ops-contract 0.9.0 → 0.9.2
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/actuation.d.ts +34 -0
- package/dist/contract.d.ts +70 -2
- package/dist/contract.js +80 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/reliability.d.ts +44 -0
- package/dist/reliability.js +105 -0
- package/dist/webhook-signature.js +1 -1
- package/dist/webhook.d.ts +1 -1
- package/dist/webhook.js +1 -1
- package/dist/yield.d.ts +97 -0
- package/dist/yield.js +82 -0
- package/dist-cjs/index.cjs +184 -60
- package/package.json +1 -1
package/dist/actuation.d.ts
CHANGED
|
@@ -14,6 +14,32 @@ export type CommandState = (typeof COMMAND_STATE)[number];
|
|
|
14
14
|
/** 이 커맨드를 낸 것이 누구인가 — 사람이 승인할 때 그 판단의 재료가 된다. */
|
|
15
15
|
export declare const COMMAND_ORIGIN: readonly ["operator", "rule", "ai", "schedule"];
|
|
16
16
|
export type CommandOrigin = (typeof COMMAND_ORIGIN)[number];
|
|
17
|
+
/**
|
|
18
|
+
* 넘긴 조치가 **저쪽에서 실제로 무엇이 되었나** — 상태가 아니라 별개 사실이다.
|
|
19
|
+
*
|
|
20
|
+
* ── 왜 상태를 늘리지 않나 (2026-09-02) ────────────────────────────────────
|
|
21
|
+
* `acked` 는 「저쪽이 답했다」이고 종착 상태다. 여기에 「현장에 나갔다」를 상태로 이어 붙이면
|
|
22
|
+
* `acked` 가 종착이 아니게 되고, **되돌아오는 관측이 없는 커넥터의 조치가 영원히 끝나지 않은 것으로
|
|
23
|
+
* 보인다.** 그런 커넥터가 정상이다 — 관측을 되돌려 주는 능력은 커넥터의 성질이지 조치의 수명이 아니다.
|
|
24
|
+
*
|
|
25
|
+
* 그래서 수명(상태)과 효과(이 칸)를 갈랐다. 효과가 없는 조치는 그냥 효과가 없는 것이고, 화면은
|
|
26
|
+
* 「넘겼다」와 「저쪽에서 확인됐다」를 따로 말할 수 있다.
|
|
27
|
+
*
|
|
28
|
+
* ── 무엇이 효과인지는 커널이 정하지 않는다 ────────────────────────────────
|
|
29
|
+
* `status` 를 **그대로 적는다.** 오더 상태 어휘는 도메인 소유이고(§`OrderStatusDelta.status`), 어느
|
|
30
|
+
* 값이 「현장에 나갔다」인지는 현장마다 다르다. 커널이 그 방언을 알면 보편 계약이 깨진다.
|
|
31
|
+
*
|
|
32
|
+
* 커널이 아는 것은 **이어졌다**는 사실뿐이다 — 우리가 낸 지시의 식별자(`dispatchRef`)로 저쪽이
|
|
33
|
+
* 보고한 것이 돌아왔다. 그 값이 무엇을 뜻하는지는 그 어휘를 아는 쪽이 읽는다.
|
|
34
|
+
*/
|
|
35
|
+
export interface CommandEffect {
|
|
36
|
+
/** 저쪽이 보고한 상태 — **해석하지 않고 그대로.** */
|
|
37
|
+
status: string;
|
|
38
|
+
/** 그 보고를 관측한 시각. */
|
|
39
|
+
at: ISOTime;
|
|
40
|
+
/** 몇 번 보고됐나 — 상태가 여러 번 바뀌면 마지막 것이 위에 남고 이 수가 는다. */
|
|
41
|
+
seen: number;
|
|
42
|
+
}
|
|
17
43
|
/** 사람이 승인하거나 거절한 기록. **재기동을 넘어 살아야 한다.** */
|
|
18
44
|
export interface CommandApproval {
|
|
19
45
|
by: string;
|
|
@@ -48,6 +74,14 @@ export interface TwinCommand {
|
|
|
48
74
|
approval?: CommandApproval;
|
|
49
75
|
/** 어댑터가 돌려준 것 — 저쪽에서 무엇이 되었는지 되짚을 수 있어야 한다. */
|
|
50
76
|
dispatchRef?: string;
|
|
77
|
+
/**
|
|
78
|
+
* 저쪽에서 실제로 무엇이 되었나 — **`dispatchRef` 가 없으면 이어 줄 길이 없다.**
|
|
79
|
+
*
|
|
80
|
+
* 넘기다 닿지 못한 조치는 식별자를 못 받는다. 그러면 저쪽이 실제로 받았는지 우리는 알 수 없고,
|
|
81
|
+
* 중복을 막는 것은 저쪽의 멱등성이다. 그 사실을 여기서 숨기지 않는다 — 이 칸이 빈 것은 「효과가
|
|
82
|
+
* 없었다」가 아니라 **「모른다」**다.
|
|
83
|
+
*/
|
|
84
|
+
effect?: CommandEffect;
|
|
51
85
|
error?: string;
|
|
52
86
|
}
|
|
53
87
|
/**
|
package/dist/contract.d.ts
CHANGED
|
@@ -3072,10 +3072,78 @@ export interface AssetStatusDelta extends EffectivePeriod {
|
|
|
3072
3072
|
* 셋을 전이로 담을 수 없다 — 사유가 붙을 자리가 없고(기계가 서는 순간에는 왜 섰는지 아무도 모른다),
|
|
3073
3073
|
* 계획·비계획을 나중에 정하며, 전이를 내지 않는 정지(자재 대기 · 작업자 부재)가 많다.
|
|
3074
3074
|
*/
|
|
3075
|
+
/**
|
|
3076
|
+
* 설비가 그 구간에 어떤 상태였나 — **닫힌 목록이다.**
|
|
3077
|
+
*
|
|
3078
|
+
* ── 왜 닫나 (2026-09-03) ────────────────────────────────────────────────────
|
|
3079
|
+
* 예전에는 `status: string` 이고 주석에만 다섯이 적혀 있었다. 그래서 커넥터가 `broken` 을 보내면
|
|
3080
|
+
* **아무 오류 없이** 고장이 0건이 되고, 가동 시간 지표가 「고장이 없었다」로 답한다. 처분 결정을
|
|
3081
|
+
* 열린 문자열로 두지 않는 이유와 같다(§`DISPOSITION_DECISION`) — 이 값이 **판정에 효과를 준다.**
|
|
3082
|
+
*
|
|
3083
|
+
* ISO 22400-2 의 시간 모델을 덮는다.
|
|
3084
|
+
*
|
|
3085
|
+
* ```
|
|
3086
|
+
* busy 실 생산(APT) — 만들고 있었다
|
|
3087
|
+
* setup 준비(AUST)
|
|
3088
|
+
* down 고장(ADOT) — 가동 시간 지표가 세는 것
|
|
3089
|
+
* idle 대기(ADET) — 자재·지시를 기다렸다. 고장이 아니다
|
|
3090
|
+
* planned-stop 계획정지 — 계획 조업 시간에서 뺀다(점심·예방보전·교대)
|
|
3091
|
+
* ```
|
|
3092
|
+
*
|
|
3093
|
+
* **`down` 과 `planned-stop` 을 가르는 것이 이 목록의 요점이다.** 둘을 한 낱말로 묶으면 예방보전을
|
|
3094
|
+
* 할수록 설비가 고장난 것으로 보이고, 그러면 보전을 미루는 쪽이 지표에 유리해진다.
|
|
3095
|
+
*
|
|
3096
|
+
* 왜 이 값이 「사유」와 다른 축인가 — 사유(`reasonCode`)는 현장이 정하는 열린 값이고, 이 값은 우리가
|
|
3097
|
+
* 정한 닫힌 분류다. 사유를 우리가 가르려 하면 현장마다 코드가 달라 못 가른다.
|
|
3098
|
+
*/
|
|
3099
|
+
export declare const EQUIPMENT_STATUS: readonly ["busy", "setup", "down", "idle", "planned-stop"];
|
|
3100
|
+
export type EquipmentStatus = (typeof EQUIPMENT_STATUS)[number];
|
|
3101
|
+
/** 선언된 상태인가. */
|
|
3102
|
+
export declare function isEquipmentStatus(value: unknown): value is EquipmentStatus;
|
|
3103
|
+
/**
|
|
3104
|
+
* 원본의 낱말을 우리 상태로 옮긴다 — **모르면 `undefined` 다.**
|
|
3105
|
+
*
|
|
3106
|
+
* ── 옮길 표가 비어 있다. 일부러 비웠다 (2026-09-03) ─────────────────────────
|
|
3107
|
+
* 처음에 `running`·`in-use`·`breakdown`·`stopped` 같은 흔한 동의어를 넣었다. **근거가 없었다.**
|
|
3108
|
+
* 세어 보니 이렇다.
|
|
3109
|
+
*
|
|
3110
|
+
* ```
|
|
3111
|
+
* operato-plant busy·setup·down·idle·planned-stop 다섯만. 동의어 0곳 (그 레인이 세어 줌)
|
|
3112
|
+
* ppms 커넥터 busy·idle·down 만
|
|
3113
|
+
* chef · ems-scada 설비 상태를 안 보낸다
|
|
3114
|
+
* ```
|
|
3115
|
+
*
|
|
3116
|
+
* 커널의 `isRunningStatus` 가 `busy`·`in-use`·`running` 셋을 받는 것을 근거로 삼았는데, 그것도
|
|
3117
|
+
* 방어였다 — `running` 을 만드는 코드가 **한 곳도 없고**, `in-use` 는 **자산의 낱말**이다
|
|
3118
|
+
* (`flow-engine.ts` 가 자산에 그 값을 준다). 그러니 `in-use → busy` 는 두 축을 섞는 것이고,
|
|
3119
|
+
* 이 저장소가 다른 자리에서 계속 거절하는 모양이다.
|
|
3120
|
+
*
|
|
3121
|
+
* 소비처가 있는 것만 적는다 — `MES_COMMANDS` 가 같은 규율을 지킨다. 실제로 다른 낱말을 보내는
|
|
3122
|
+
* 원본을 만나면 **그 낱말을 여기 적는다.** 그때 이 함수가 그 자리가 되고, 커넥터마다 다시 적어
|
|
3123
|
+
* 흩어지지 않는다.
|
|
3124
|
+
*
|
|
3125
|
+
* 지금 하는 일은 둘이다 — 대소문자·공백을 다듬고, **모르는 낱말을 `undefined` 로 답한다.**
|
|
3126
|
+
* 모름을 `idle` 로 채우면 고장이 조용히 사라지고, `down` 으로 채우면 부풀린다.
|
|
3127
|
+
*/
|
|
3128
|
+
export declare function normalizeEquipmentStatus(value: unknown): EquipmentStatus | undefined;
|
|
3129
|
+
/**
|
|
3130
|
+
* 고장이었나 — **가동 시간 지표(MTBF)가 세는 것.**
|
|
3131
|
+
*
|
|
3132
|
+
* 계획정지·준비·대기는 고장이 아니다. 이 판정을 부르는 쪽마다 다시 쓰면 한 곳이 `planned-stop` 을
|
|
3133
|
+
* 고장으로 세는 날이 오고, 그때 두 화면이 다른 수를 말한다.
|
|
3134
|
+
*/
|
|
3135
|
+
export declare function isFailureStatus(value: unknown): boolean;
|
|
3136
|
+
/** 계획정지였나 — 계획 조업 시간에서 빼는 것(§`OeeCounters.holdMs`). */
|
|
3137
|
+
export declare function isPlannedStopStatus(value: unknown): boolean;
|
|
3075
3138
|
export interface EquipmentStatePeriodFact {
|
|
3076
3139
|
moverId: string;
|
|
3077
|
-
/**
|
|
3078
|
-
|
|
3140
|
+
/**
|
|
3141
|
+
* 그 구간의 상태 — **선언된 다섯 중 하나**(§`EQUIPMENT_STATUS`).
|
|
3142
|
+
*
|
|
3143
|
+
* 원본의 낱말이 다르면 커넥터가 `normalizeEquipmentStatus` 로 옮긴다. 옮길 수 없는 값은 이 사실을
|
|
3144
|
+
* 만들지 말고 그 사실을 말해야 한다 — 모르는 상태를 아무 것으로 채우면 지표가 조용히 틀린다.
|
|
3145
|
+
*/
|
|
3146
|
+
status: EquipmentStatus;
|
|
3079
3147
|
from: ISOTime;
|
|
3080
3148
|
to: ISOTime;
|
|
3081
3149
|
/** 사람이 정한 사유. 없을 수 있다 — 적지 않은 것과 사유가 없는 것은 다르므로 지어내지 않는다. */
|
package/dist/contract.js
CHANGED
|
@@ -1186,6 +1186,86 @@ export const ENERGY_EVENT = {
|
|
|
1186
1186
|
* 커널은 이 값으로 판정하지 않는다 — 무엇을 믿을지는 현장이 정한다.
|
|
1187
1187
|
*/
|
|
1188
1188
|
export const OBSERVATION_BASIS = ['metered', 'provider', 'billed', 'estimated'];
|
|
1189
|
+
/**
|
|
1190
|
+
* **마감된 설비 상태 구간** — 이 설비가 이 구간 동안 이 상태였다.
|
|
1191
|
+
*
|
|
1192
|
+
* 시점의 전이(`EquipmentStatusDelta`)와 다른 사실이다. 전이는 「지금 이 상태로 바뀌었다」이고 이것은
|
|
1193
|
+
* 「그 구간 동안 그 상태였다」다. 되돌아가 적는 사실이라 사건 시각이 `to` 다.
|
|
1194
|
+
*
|
|
1195
|
+
* 셋을 전이로 담을 수 없다 — 사유가 붙을 자리가 없고(기계가 서는 순간에는 왜 섰는지 아무도 모른다),
|
|
1196
|
+
* 계획·비계획을 나중에 정하며, 전이를 내지 않는 정지(자재 대기 · 작업자 부재)가 많다.
|
|
1197
|
+
*/
|
|
1198
|
+
/**
|
|
1199
|
+
* 설비가 그 구간에 어떤 상태였나 — **닫힌 목록이다.**
|
|
1200
|
+
*
|
|
1201
|
+
* ── 왜 닫나 (2026-09-03) ────────────────────────────────────────────────────
|
|
1202
|
+
* 예전에는 `status: string` 이고 주석에만 다섯이 적혀 있었다. 그래서 커넥터가 `broken` 을 보내면
|
|
1203
|
+
* **아무 오류 없이** 고장이 0건이 되고, 가동 시간 지표가 「고장이 없었다」로 답한다. 처분 결정을
|
|
1204
|
+
* 열린 문자열로 두지 않는 이유와 같다(§`DISPOSITION_DECISION`) — 이 값이 **판정에 효과를 준다.**
|
|
1205
|
+
*
|
|
1206
|
+
* ISO 22400-2 의 시간 모델을 덮는다.
|
|
1207
|
+
*
|
|
1208
|
+
* ```
|
|
1209
|
+
* busy 실 생산(APT) — 만들고 있었다
|
|
1210
|
+
* setup 준비(AUST)
|
|
1211
|
+
* down 고장(ADOT) — 가동 시간 지표가 세는 것
|
|
1212
|
+
* idle 대기(ADET) — 자재·지시를 기다렸다. 고장이 아니다
|
|
1213
|
+
* planned-stop 계획정지 — 계획 조업 시간에서 뺀다(점심·예방보전·교대)
|
|
1214
|
+
* ```
|
|
1215
|
+
*
|
|
1216
|
+
* **`down` 과 `planned-stop` 을 가르는 것이 이 목록의 요점이다.** 둘을 한 낱말로 묶으면 예방보전을
|
|
1217
|
+
* 할수록 설비가 고장난 것으로 보이고, 그러면 보전을 미루는 쪽이 지표에 유리해진다.
|
|
1218
|
+
*
|
|
1219
|
+
* 왜 이 값이 「사유」와 다른 축인가 — 사유(`reasonCode`)는 현장이 정하는 열린 값이고, 이 값은 우리가
|
|
1220
|
+
* 정한 닫힌 분류다. 사유를 우리가 가르려 하면 현장마다 코드가 달라 못 가른다.
|
|
1221
|
+
*/
|
|
1222
|
+
export const EQUIPMENT_STATUS = ['busy', 'setup', 'down', 'idle', 'planned-stop'];
|
|
1223
|
+
/** 선언된 상태인가. */
|
|
1224
|
+
export function isEquipmentStatus(value) {
|
|
1225
|
+
return typeof value === 'string' && EQUIPMENT_STATUS.includes(value);
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* 원본의 낱말을 우리 상태로 옮긴다 — **모르면 `undefined` 다.**
|
|
1229
|
+
*
|
|
1230
|
+
* ── 옮길 표가 비어 있다. 일부러 비웠다 (2026-09-03) ─────────────────────────
|
|
1231
|
+
* 처음에 `running`·`in-use`·`breakdown`·`stopped` 같은 흔한 동의어를 넣었다. **근거가 없었다.**
|
|
1232
|
+
* 세어 보니 이렇다.
|
|
1233
|
+
*
|
|
1234
|
+
* ```
|
|
1235
|
+
* operato-plant busy·setup·down·idle·planned-stop 다섯만. 동의어 0곳 (그 레인이 세어 줌)
|
|
1236
|
+
* ppms 커넥터 busy·idle·down 만
|
|
1237
|
+
* chef · ems-scada 설비 상태를 안 보낸다
|
|
1238
|
+
* ```
|
|
1239
|
+
*
|
|
1240
|
+
* 커널의 `isRunningStatus` 가 `busy`·`in-use`·`running` 셋을 받는 것을 근거로 삼았는데, 그것도
|
|
1241
|
+
* 방어였다 — `running` 을 만드는 코드가 **한 곳도 없고**, `in-use` 는 **자산의 낱말**이다
|
|
1242
|
+
* (`flow-engine.ts` 가 자산에 그 값을 준다). 그러니 `in-use → busy` 는 두 축을 섞는 것이고,
|
|
1243
|
+
* 이 저장소가 다른 자리에서 계속 거절하는 모양이다.
|
|
1244
|
+
*
|
|
1245
|
+
* 소비처가 있는 것만 적는다 — `MES_COMMANDS` 가 같은 규율을 지킨다. 실제로 다른 낱말을 보내는
|
|
1246
|
+
* 원본을 만나면 **그 낱말을 여기 적는다.** 그때 이 함수가 그 자리가 되고, 커넥터마다 다시 적어
|
|
1247
|
+
* 흩어지지 않는다.
|
|
1248
|
+
*
|
|
1249
|
+
* 지금 하는 일은 둘이다 — 대소문자·공백을 다듬고, **모르는 낱말을 `undefined` 로 답한다.**
|
|
1250
|
+
* 모름을 `idle` 로 채우면 고장이 조용히 사라지고, `down` 으로 채우면 부풀린다.
|
|
1251
|
+
*/
|
|
1252
|
+
export function normalizeEquipmentStatus(value) {
|
|
1253
|
+
const said = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
1254
|
+
return isEquipmentStatus(said) ? said : undefined;
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* 고장이었나 — **가동 시간 지표(MTBF)가 세는 것.**
|
|
1258
|
+
*
|
|
1259
|
+
* 계획정지·준비·대기는 고장이 아니다. 이 판정을 부르는 쪽마다 다시 쓰면 한 곳이 `planned-stop` 을
|
|
1260
|
+
* 고장으로 세는 날이 오고, 그때 두 화면이 다른 수를 말한다.
|
|
1261
|
+
*/
|
|
1262
|
+
export function isFailureStatus(value) {
|
|
1263
|
+
return normalizeEquipmentStatus(value) === 'down';
|
|
1264
|
+
}
|
|
1265
|
+
/** 계획정지였나 — 계획 조업 시간에서 빼는 것(§`OeeCounters.holdMs`). */
|
|
1266
|
+
export function isPlannedStopStatus(value) {
|
|
1267
|
+
return normalizeEquipmentStatus(value) === 'planned-stop';
|
|
1268
|
+
}
|
|
1189
1269
|
// ── Command 채널 어휘 — 트윈의 "행위(act)" 면 (prescriptive/트랜잭션 프론트엔드) ──
|
|
1190
1270
|
// 코어 공통: order.hold/resume(할당 보류). 도메인: order.release(즉시 투입) 등은 handleCommand 로.
|
|
1191
1271
|
export const CMD = {
|
package/dist/index.d.ts
CHANGED
|
@@ -19,5 +19,7 @@ export * from './yms-profile.ts';
|
|
|
19
19
|
export * from './canonical-record.ts';
|
|
20
20
|
export * from './webhook.ts';
|
|
21
21
|
export * from './oee.ts';
|
|
22
|
+
export * from './yield.ts';
|
|
23
|
+
export * from './reliability.ts';
|
|
22
24
|
export * from './erp.ts';
|
|
23
25
|
export * from './actuation.ts';
|
package/dist/index.js
CHANGED
|
@@ -41,5 +41,7 @@ export * from "./canonical-record.js";
|
|
|
41
41
|
/* `webhook-signature.ts` 는 여기 없다 — `node:crypto` 를 쓰므로 `@operato/ops-contract/webhook` 으로만 나간다. */
|
|
42
42
|
export * from "./webhook.js";
|
|
43
43
|
export * from "./oee.js";
|
|
44
|
+
export * from "./yield.js";
|
|
45
|
+
export * from "./reliability.js";
|
|
44
46
|
export * from "./erp.js";
|
|
45
47
|
export * from "./actuation.js";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ISOTime } from './contract.ts';
|
|
2
|
+
/** 상태 구간 하나 — `EquipmentStatePeriodFact` 에서 셋만. */
|
|
3
|
+
export interface ReliabilityPeriod {
|
|
4
|
+
status: string;
|
|
5
|
+
from: ISOTime;
|
|
6
|
+
to: ISOTime;
|
|
7
|
+
}
|
|
8
|
+
export interface MtbfInput {
|
|
9
|
+
/** 어느 설비의. */
|
|
10
|
+
equipmentId: string;
|
|
11
|
+
/** 그 설비의 상태 구간들. 순서는 상관없다 — 여기서 시각으로 세운다. */
|
|
12
|
+
periods: readonly ReliabilityPeriod[];
|
|
13
|
+
}
|
|
14
|
+
/** 낼 수 없는 이유 — 「0」이나 「무한」으로 답하지 않는다. */
|
|
15
|
+
export type MtbfMissing =
|
|
16
|
+
/** 돈 시간이 없다 — 잰 구간에 `busy` 가 하나도 없다. */
|
|
17
|
+
'no-operating-time'
|
|
18
|
+
/** 고장이 없다 — 나눌 수가 0이다. 좋은 수가 아니라 **답할 수 없는** 것이다. */
|
|
19
|
+
| 'no-failure'
|
|
20
|
+
/** 모르는 상태 낱말이 있다 — 조용히 빼면 고장이 사라진다. */
|
|
21
|
+
| 'unknown-status'
|
|
22
|
+
/** 길이가 없거나 시각을 읽을 수 없는 구간이 있다. */
|
|
23
|
+
| 'invalid-period';
|
|
24
|
+
export interface Mtbf {
|
|
25
|
+
equipmentId: string;
|
|
26
|
+
/** 고장 사이 평균 가동 시간(ms). **낼 수 없으면 없다.** */
|
|
27
|
+
mtbfMs?: number;
|
|
28
|
+
/** 분자 — `busy` 의 합. */
|
|
29
|
+
operatingMs: number;
|
|
30
|
+
/** 분모 — **고장으로 들어간 횟수**(이어진 `down` 은 한 번). */
|
|
31
|
+
failures: number;
|
|
32
|
+
/** 받은 `down` 구간의 수 — `failures` 와 다르면 이어진 것이 있었다. */
|
|
33
|
+
downPeriods: number;
|
|
34
|
+
/** 옮길 수 없던 상태 낱말들 — 무엇이 왔는지 말한다. */
|
|
35
|
+
unknownStatuses: string[];
|
|
36
|
+
missing: MtbfMissing[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 이 설비의 MTBF.
|
|
40
|
+
*
|
|
41
|
+
* **모르는 상태를 만나면 답하지 않는다.** 빼고 계산하면 그 구간이 고장이었을 때 고장이 조용히
|
|
42
|
+
* 사라지고, 그러면 이 수가 실제보다 좋아진다 — 자료가 나쁠수록 수가 좋아지는 쪽은 두지 않는다.
|
|
43
|
+
*/
|
|
44
|
+
export declare function computeMtbf(input: MtbfInput): Mtbf;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **MTBF** — ISO 22400-2. 고장 사이에 얼마나 돌았나.
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 구간을 받나 (집계 값이 아니라) ────────────────────────────────────────
|
|
5
|
+
* `computeMtbf(가동시간, 고장횟수)` 로 두면 **「고장 횟수」를 부르는 쪽이 센다.** 그러면 세는 방법이
|
|
6
|
+
* 갈리고, 갈린 것을 아무도 모른다 — 오늘 이 저장소에서 여러 번 본 부류다.
|
|
7
|
+
*
|
|
8
|
+
* 특히 한 가지를 부르는 쪽이 거의 틀린다(아래 §고장 하나). 그래서 구간을 그대로 받아 세는 것까지
|
|
9
|
+
* 계약이 한다.
|
|
10
|
+
*
|
|
11
|
+
* ── 분자는 **실 가동 시간**이다 ──────────────────────────────────────────────
|
|
12
|
+
* `busy` 만 더한다. 달력 시간이 아니고, `setup` 도 아니다.
|
|
13
|
+
*
|
|
14
|
+
* 표준의 시간 모델에서 준비(AUST)는 실 생산 시간(APT)이 아니다. 준비를 더하면 **준비가 긴 설비의
|
|
15
|
+
* MTBF 가 좋아진다** — 고장 사이에 더 오래 「돌았다」고 세어지기 때문이다. 그것은 이 수가 답하려는
|
|
16
|
+
* 물음이 아니다.
|
|
17
|
+
*
|
|
18
|
+
* 대기(`idle`)와 계획정지(`planned-stop`)도 안 더한다. 서 있는 동안에는 고장날 일이 없다.
|
|
19
|
+
*
|
|
20
|
+
* ── 고장 하나 — **이어진 `down` 은 한 번이다** ───────────────────────────────
|
|
21
|
+
* 원본이 정지를 토막으로 적는 일이 흔하다. 교대가 갈릴 때, 사유를 나중에 고칠 때, 날짜가 바뀔 때.
|
|
22
|
+
*
|
|
23
|
+
* ```
|
|
24
|
+
* down 08:00–14:00 · down 14:00–20:00 한 번 고장난 것이다
|
|
25
|
+
* 그것을 두 번으로 세면 MTBF 가 절반이 된다
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* 그래서 구간의 수를 세지 않고 **고장으로 들어간 횟수**를 센다. 이어진 `down` 은 들어간 것이 한 번
|
|
29
|
+
* 이므로 한 번이다. 이 셈을 부르는 쪽에 맡기면 거의 틀리고, 틀린 쪽이 늘 **더 나쁜 수**를 낸다.
|
|
30
|
+
*
|
|
31
|
+
* ── 고장이 없으면 답하지 않는다 ──────────────────────────────────────────────
|
|
32
|
+
* 나누는 수가 0이다. 「무한히 안 고장났다」로 답하면 화면이 그것을 가장 좋은 수로 그리는데, 실제로는
|
|
33
|
+
* **아직 한 번도 안 고장난 것**이고 그 기간이 한 시간일 수도 있다.
|
|
34
|
+
*
|
|
35
|
+
* 그때는 돌았던 시간과 고장 0을 그대로 낸다 — 사람이 그 둘을 보고 판단한다.
|
|
36
|
+
*/
|
|
37
|
+
import { normalizeEquipmentStatus } from "./contract.js";
|
|
38
|
+
const at = (t) => (typeof t === 'string' ? Date.parse(t) : NaN);
|
|
39
|
+
/**
|
|
40
|
+
* 이 설비의 MTBF.
|
|
41
|
+
*
|
|
42
|
+
* **모르는 상태를 만나면 답하지 않는다.** 빼고 계산하면 그 구간이 고장이었을 때 고장이 조용히
|
|
43
|
+
* 사라지고, 그러면 이 수가 실제보다 좋아진다 — 자료가 나쁠수록 수가 좋아지는 쪽은 두지 않는다.
|
|
44
|
+
*/
|
|
45
|
+
export function computeMtbf(input) {
|
|
46
|
+
const { equipmentId } = input;
|
|
47
|
+
const missing = [];
|
|
48
|
+
const unknown = new Set();
|
|
49
|
+
/* 시각으로 세운다 — 받은 순서를 믿지 않는다. 이어짐 판정이 순서에 걸린다. */
|
|
50
|
+
const sorted = [...(input.periods ?? [])]
|
|
51
|
+
.map(p => ({ status: normalizeEquipmentStatus(p?.status), said: String(p?.status ?? ''), from: at(p?.from), to: at(p?.to) }))
|
|
52
|
+
.sort((a, b) => a.from - b.from);
|
|
53
|
+
let operatingMs = 0;
|
|
54
|
+
let failures = 0;
|
|
55
|
+
let downPeriods = 0;
|
|
56
|
+
let invalid = false;
|
|
57
|
+
/* 앞 구간이 고장이었나 — 이어진 `down` 을 한 번으로 세는 열쇠다. */
|
|
58
|
+
let wasDown = false;
|
|
59
|
+
for (const p of sorted) {
|
|
60
|
+
if (!Number.isFinite(p.from) || !Number.isFinite(p.to) || p.to <= p.from) {
|
|
61
|
+
invalid = true;
|
|
62
|
+
/* 길이 없는 구간은 이어짐 판정도 흐트린다 — 앞 상태를 그대로 두고 넘긴다. */
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (p.status === undefined) {
|
|
66
|
+
unknown.add(p.said);
|
|
67
|
+
/*
|
|
68
|
+
* 모르는 낱말은 **이어짐을 끊는다.** 그것이 고장이었는지 아닌지 모르므로, 앞뒤의 `down` 을
|
|
69
|
+
* 한 번으로 묶어도 두 번으로 세도 근거가 없다. 어차피 답하지 않을 것이라 여기서는 끊어 둔다.
|
|
70
|
+
*/
|
|
71
|
+
wasDown = false;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (p.status === 'busy')
|
|
75
|
+
operatingMs += p.to - p.from;
|
|
76
|
+
if (p.status === 'down') {
|
|
77
|
+
downPeriods++;
|
|
78
|
+
if (!wasDown)
|
|
79
|
+
failures++;
|
|
80
|
+
wasDown = true;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
wasDown = false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (invalid)
|
|
87
|
+
missing.push('invalid-period');
|
|
88
|
+
if (unknown.size)
|
|
89
|
+
missing.push('unknown-status');
|
|
90
|
+
if (operatingMs <= 0)
|
|
91
|
+
missing.push('no-operating-time');
|
|
92
|
+
if (failures === 0)
|
|
93
|
+
missing.push('no-failure');
|
|
94
|
+
const out = {
|
|
95
|
+
equipmentId,
|
|
96
|
+
operatingMs,
|
|
97
|
+
failures,
|
|
98
|
+
downPeriods,
|
|
99
|
+
unknownStatuses: [...unknown],
|
|
100
|
+
missing
|
|
101
|
+
};
|
|
102
|
+
if (missing.length)
|
|
103
|
+
return out;
|
|
104
|
+
return { ...out, mtbfMs: operatingMs / failures };
|
|
105
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* 서명은 양쪽이 **한 글자까지 같은 규칙**을 써야 성립한다. 한쪽이 이어붙이는 순서를 바꾸면 다른 쪽은
|
|
6
6
|
* 401 만 보고 이유를 알 수 없다. 그래서 만드는 함수와 확인하는 함수가 한 파일에 있어야 한다.
|
|
7
7
|
*
|
|
8
|
-
* 실제로 operato-
|
|
8
|
+
* 실제로 operato-plant 안에 `sign` 과 `verifySignature` 가 함께 있었는데, `verifySignature` 는 저장소
|
|
9
9
|
* 어디에서도 불리지 않았다 — 확인할 수신부가 없었기 때문이다. 확인하는 쪽이 생기면서 그 함수의 자리가
|
|
10
10
|
* 여기가 됐다.
|
|
11
11
|
*
|
package/dist/webhook.d.ts
CHANGED
|
@@ -55,7 +55,7 @@ export declare const WEBHOOK_TOO_MANY = 429;
|
|
|
55
55
|
* 봉투 하나 — 번호와 사실을 함께 든다.
|
|
56
56
|
*
|
|
57
57
|
* `scope` 가 **번호를 매기는 단위**다. 이것이 없으면 받는 쪽은 그 번호가 어느 줄의 번호인지 모르고,
|
|
58
|
-
* 커서를 무엇으로 잡을지 짐작해야 한다. 실제로 operato-
|
|
58
|
+
* 커서를 무엇으로 잡을지 짐작해야 한다. 실제로 operato-plant 가 `[domain, channel]` 마다 번호를 매기면서
|
|
59
59
|
* 봉투에는 `channel` 을 싣지 않고 있었다 — 두 채널이 흐르기 시작하면 두 줄의 번호가 한 커서에 섞여
|
|
60
60
|
* 연속성 검사가 끊임없이 「비었다」로 답한다.
|
|
61
61
|
*
|
package/dist/webhook.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* ── 왜 계약에 있나 (2026-08-31) ──────────────────────────────────────────
|
|
5
5
|
* 보내는 쪽은 응답 코드 하나로 「다시 보낼 것인가 · 기다릴 것인가 · 사람을 부를 것인가」를 정한다.
|
|
6
6
|
* 그 판단표가 양쪽에 따로 있으면 한쪽만 바뀌는 날이 온다. 실제로 그렇게 되어 있었다 — 트윈은 409 를
|
|
7
|
-
* 「그 트윈이 실시간으로 돌지 않는다」로 쓰고 operato-
|
|
7
|
+
* 「그 트윈이 실시간으로 돌지 않는다」로 쓰고 operato-plant 는 409 를 「번호가 비었다」로 읽었다.
|
|
8
8
|
* 붙였다면 트윈이 「안 돈다」고 답하는 것을 MES 가 「번호가 비었다」로 읽어 같은 구간을 되풀이 보냈다.
|
|
9
9
|
*
|
|
10
10
|
* 계약 층의 기준이 「만드는 쪽과 읽는 쪽이 합의해야 하는가」이고, 응답 코드와 번호 규칙이 그것이다.
|
package/dist/yield.d.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { ISOTime } from './contract.ts';
|
|
2
|
+
/** 직행수율 하나의 단위 — **공정 하나**다. 경로 전체는 이름이 다르다(§아래). */
|
|
3
|
+
export interface FirstPassInput {
|
|
4
|
+
/** 어느 작업지시의. */
|
|
5
|
+
jobOrderName: string;
|
|
6
|
+
/** 어느 공정의 — 실적이 이 값을 들고 있다. */
|
|
7
|
+
operationKey: string;
|
|
8
|
+
/**
|
|
9
|
+
* 그 공정에 **들어간 수**. 모르면 적지 않는다 — 아래 둘의 합이 대신 선다.
|
|
10
|
+
*
|
|
11
|
+
* **분모가 아니다.** 분모는 마친 수(양품+부적합)이고, 이 값은 **창이 닫혔는지**를 말한다 — 들어간
|
|
12
|
+
* 것보다 마친 것이 적으면 아직 공정에 남아 있다.
|
|
13
|
+
*/
|
|
14
|
+
entered?: number;
|
|
15
|
+
/** 처음에 통과한 수 — **재작업해서 통과한 것을 여기 더하지 않는다**(§`FIRST_PASS_RULE`). */
|
|
16
|
+
good: number;
|
|
17
|
+
/**
|
|
18
|
+
* **부적합 처분이 달린 수** — 종류를 가리지 않는다.
|
|
19
|
+
*
|
|
20
|
+
* 이름을 `scrap` 으로 두지 않는다. 실적의 그 칸은 「부적합 전체」를 세는데 처분 어휘의 `scrap` 은
|
|
21
|
+
* 「폐기」 하나다(§`DISPOSITION_DECISION`). 같은 낱말이 두 뜻이라, 부르는 쪽이 「폐기 수」로 읽으면
|
|
22
|
+
* 재작업된 것이 분모에서 빠져 **직행수율이 100%로 나온다.** 오류는 안 난다.
|
|
23
|
+
*/
|
|
24
|
+
nonconforming: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 분모는 **그 공정을 마친 수**다 — 양품과 부적합의 합.
|
|
28
|
+
*
|
|
29
|
+
* ── 왜 「들어간 수」가 분모가 아닌가 (첫 판을 고침) ─────────────────────────
|
|
30
|
+
* 처음에는 `들어간 수 − 부적합` 을 분자로 뒀다. **틀렸다.** 들어갔는데 아직 안 나온 것이 그 계산에서
|
|
31
|
+
* 「처음에 통과」로 세어진다 — 158이 들어가고 154가 나왔는데 160으로 잰다면 2개를 통과로 센다.
|
|
32
|
+
*
|
|
33
|
+
* 마친 수로 세면 그 수가 참이다. `entered` 는 분모가 아니라 **창이 닫혔는지**를 말하는 데 쓴다.
|
|
34
|
+
*/
|
|
35
|
+
export type FirstPassBasis = 'completed';
|
|
36
|
+
/** 낼 수 없는 이유 — 「0%」로 답하지 않는다. */
|
|
37
|
+
export type FirstPassMissing =
|
|
38
|
+
/** 마친 것이 없다 — 아직 아무 실적이 없다. */
|
|
39
|
+
'no-input'
|
|
40
|
+
/** 마친 수가 들어간 수보다 많다 — 재작업 산출을 되더한 자료가 여기서 걸린다. */
|
|
41
|
+
| 'inconsistent-counts'
|
|
42
|
+
/** 수가 아니거나 음수다. */
|
|
43
|
+
| 'invalid-counts';
|
|
44
|
+
export interface FirstPassYield {
|
|
45
|
+
jobOrderName: string;
|
|
46
|
+
operationKey: string;
|
|
47
|
+
/** 0~1. **낼 수 없으면 없다** — 0 으로 채우지 않는다. */
|
|
48
|
+
fpy?: number;
|
|
49
|
+
/** 분자로 쓴 수 — 처음에 통과한 수. */
|
|
50
|
+
firstPassGood: number;
|
|
51
|
+
/** 분모로 쓴 수 — 그 공정을 마친 수. */
|
|
52
|
+
completed: number;
|
|
53
|
+
basis: FirstPassBasis;
|
|
54
|
+
/**
|
|
55
|
+
* 아직 그 공정에 있는 수 — **`entered` 를 알 때만.**
|
|
56
|
+
*
|
|
57
|
+
* 이 값이 0보다 크면 창이 열려 있고, 그때의 수율은 「지금까지 마친 것」의 수율이다. 모르면 비어
|
|
58
|
+
* 있고, 그것은 「0개가 남았다」와 다르다.
|
|
59
|
+
*/
|
|
60
|
+
inProcess?: number;
|
|
61
|
+
/** 낼 수 없던 이유들. 비어 있으면 낸 것이다. */
|
|
62
|
+
missing: FirstPassMissing[];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* **이 수가 직행수율이 되기 위한 규율** — 계약이 글로 들고, 두 쪽이 같은 것을 지킨다.
|
|
66
|
+
*
|
|
67
|
+
* 트윈과 MES 가 각자 이 문장을 지키지 않으면 같은 이름으로 다른 수가 나오고, 그때 어느 쪽이 맞는지
|
|
68
|
+
* 말할 수 없다.
|
|
69
|
+
*/
|
|
70
|
+
export declare const FIRST_PASS_RULE = "\uC7AC\uC791\uC5C5\u00B7\uC218\uB9AC\uB85C \uD1B5\uACFC\uD55C \uC0B0\uCD9C\uC740 \uC6D0\uB798 \uACF5\uC815 \uC2E4\uC801\uC758 \uC591\uD488\uC5D0 \uB418\uB354\uD558\uC9C0 \uC54A\uB294\uB2E4 \u2014 \uADF8\uAC83\uC744 \uB354\uD558\uBA74 \uC774 \uC218\uB294 \uD488\uC9C8\uB960\uC774 \uB41C\uB2E4";
|
|
71
|
+
/**
|
|
72
|
+
* 공정 하나의 직행수율.
|
|
73
|
+
*
|
|
74
|
+
* **경로 전체는 이 함수가 아니다.** 갈래가 합류하는 공장에서 공정별과 경로 전체는 다른 수이고, 경로
|
|
75
|
+
* 전체는 `rolledThroughputYield` 로 이름이 갈린다 — ISO 22400 의 KPI 가 아니라 다른 관행의 이름이라
|
|
76
|
+
* 섞으면 사람이 공정 하나의 수로 읽는다.
|
|
77
|
+
*/
|
|
78
|
+
export declare function computeFirstPassYield(input: FirstPassInput): FirstPassYield;
|
|
79
|
+
/**
|
|
80
|
+
* **경로 전체의 수율** — 공정별 직행수율의 곱.
|
|
81
|
+
*
|
|
82
|
+
* ISO 22400 의 KPI 가 아니다. 다른 관행(식스시그마)의 이름이고, 그래서 **이름을 갈라 둔다** —
|
|
83
|
+
* 직행수율이라고 부르면 사람이 공정 하나의 수로 읽고, 「어느 공정을 고칠까」에 답할 수 없다.
|
|
84
|
+
*
|
|
85
|
+
* 하나라도 낼 수 없으면 **전체를 내지 않는다.** 빠진 공정을 1로 두고 곱하면 그 공정이 완벽했다는
|
|
86
|
+
* 뜻이 되고, 자료가 없을수록 수가 좋아진다.
|
|
87
|
+
*/
|
|
88
|
+
export declare function rolledThroughputYield(steps: readonly FirstPassYield[]): {
|
|
89
|
+
rty?: number;
|
|
90
|
+
countedSteps: number;
|
|
91
|
+
skipped: number;
|
|
92
|
+
};
|
|
93
|
+
/** 언제 잰 것인가 — 창이 열린 채로 센 수는 분모가 작다(§머리말). */
|
|
94
|
+
export interface FirstPassWindow {
|
|
95
|
+
from?: ISOTime;
|
|
96
|
+
to?: ISOTime;
|
|
97
|
+
}
|
package/dist/yield.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **이 수가 직행수율이 되기 위한 규율** — 계약이 글로 들고, 두 쪽이 같은 것을 지킨다.
|
|
3
|
+
*
|
|
4
|
+
* 트윈과 MES 가 각자 이 문장을 지키지 않으면 같은 이름으로 다른 수가 나오고, 그때 어느 쪽이 맞는지
|
|
5
|
+
* 말할 수 없다.
|
|
6
|
+
*/
|
|
7
|
+
export const FIRST_PASS_RULE = '재작업·수리로 통과한 산출은 원래 공정 실적의 양품에 되더하지 않는다 — 그것을 더하면 이 수는 품질률이 된다';
|
|
8
|
+
const finite = (n) => typeof n === 'number' && Number.isFinite(n);
|
|
9
|
+
/**
|
|
10
|
+
* 공정 하나의 직행수율.
|
|
11
|
+
*
|
|
12
|
+
* **경로 전체는 이 함수가 아니다.** 갈래가 합류하는 공장에서 공정별과 경로 전체는 다른 수이고, 경로
|
|
13
|
+
* 전체는 `rolledThroughputYield` 로 이름이 갈린다 — ISO 22400 의 KPI 가 아니라 다른 관행의 이름이라
|
|
14
|
+
* 섞으면 사람이 공정 하나의 수로 읽는다.
|
|
15
|
+
*/
|
|
16
|
+
export function computeFirstPassYield(input) {
|
|
17
|
+
const missing = [];
|
|
18
|
+
const { jobOrderName, operationKey } = input;
|
|
19
|
+
const good = input.good;
|
|
20
|
+
const bad = input.nonconforming;
|
|
21
|
+
const known = input.entered !== undefined;
|
|
22
|
+
if (!finite(good) || !finite(bad) || good < 0 || bad < 0 || (known && (!finite(input.entered) || input.entered < 0))) {
|
|
23
|
+
return { jobOrderName, operationKey, firstPassGood: 0, completed: 0, basis: 'completed', missing: ['invalid-counts'] };
|
|
24
|
+
}
|
|
25
|
+
const completed = good + bad;
|
|
26
|
+
if (completed <= 0)
|
|
27
|
+
missing.push('no-input');
|
|
28
|
+
/*
|
|
29
|
+
* 마친 수가 들어간 수보다 많으면 **답하지 않는다.** 어느 쪽이 틀렸는지 우리가 정할 수 없고, 그때
|
|
30
|
+
* 비율을 내면 그 수가 참인 척한다. 화면이 그것을 그리면 사람이 자료를 의심하지 않고 지표를 의심한다.
|
|
31
|
+
*
|
|
32
|
+
* 이 검사가 잡는 실제 경우가 하나 있다 — **재작업 산출을 원래 실적에 되더한 자료**다. 그러면 마친
|
|
33
|
+
* 수가 들어간 수를 넘고, 여기서 거절된다(§`FIRST_PASS_RULE`).
|
|
34
|
+
*/
|
|
35
|
+
if (known && completed > input.entered)
|
|
36
|
+
missing.push('inconsistent-counts');
|
|
37
|
+
const inProcess = known ? Math.max(0, input.entered - completed) : undefined;
|
|
38
|
+
if (missing.length) {
|
|
39
|
+
return {
|
|
40
|
+
jobOrderName,
|
|
41
|
+
operationKey,
|
|
42
|
+
firstPassGood: good,
|
|
43
|
+
completed,
|
|
44
|
+
basis: 'completed',
|
|
45
|
+
...(inProcess === undefined ? {} : { inProcess }),
|
|
46
|
+
missing
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/*
|
|
50
|
+
* 분자는 **양품 수**다 — 규율이 지켜지면 그것이 곧 「처음에 통과한 수」다(§`FIRST_PASS_RULE`).
|
|
51
|
+
*
|
|
52
|
+
* **함수는 그 규율이 지켜졌는지 알 수 없다.** 재작업 산출이 그 칸에 되더해져 있으면 같은 식이 품질률을
|
|
53
|
+
* 낸다. 위의 일관성 검사가 `entered` 를 알 때만 그것을 잡는다 — 모를 때는 자료를 만드는 쪽이 규율을
|
|
54
|
+
* 지켜야 하고, 그래서 그 규율이 계약에 글로 있다.
|
|
55
|
+
*/
|
|
56
|
+
return {
|
|
57
|
+
jobOrderName,
|
|
58
|
+
operationKey,
|
|
59
|
+
fpy: good / completed,
|
|
60
|
+
firstPassGood: good,
|
|
61
|
+
completed,
|
|
62
|
+
basis: 'completed',
|
|
63
|
+
...(inProcess === undefined ? {} : { inProcess }),
|
|
64
|
+
missing
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* **경로 전체의 수율** — 공정별 직행수율의 곱.
|
|
69
|
+
*
|
|
70
|
+
* ISO 22400 의 KPI 가 아니다. 다른 관행(식스시그마)의 이름이고, 그래서 **이름을 갈라 둔다** —
|
|
71
|
+
* 직행수율이라고 부르면 사람이 공정 하나의 수로 읽고, 「어느 공정을 고칠까」에 답할 수 없다.
|
|
72
|
+
*
|
|
73
|
+
* 하나라도 낼 수 없으면 **전체를 내지 않는다.** 빠진 공정을 1로 두고 곱하면 그 공정이 완벽했다는
|
|
74
|
+
* 뜻이 되고, 자료가 없을수록 수가 좋아진다.
|
|
75
|
+
*/
|
|
76
|
+
export function rolledThroughputYield(steps) {
|
|
77
|
+
const usable = steps.filter(s => s.fpy !== undefined);
|
|
78
|
+
const skipped = steps.length - usable.length;
|
|
79
|
+
if (!usable.length || skipped > 0)
|
|
80
|
+
return { countedSteps: usable.length, skipped };
|
|
81
|
+
return { rty: usable.reduce((acc, s) => acc * s.fpy, 1), countedSteps: usable.length, skipped };
|
|
82
|
+
}
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -43,8 +43,10 @@ __export(index_exports, {
|
|
|
43
43
|
EPCIS_CONTEXT: () => EPCIS_CONTEXT,
|
|
44
44
|
EPCIS_EVENT: () => EPCIS_EVENT,
|
|
45
45
|
EQUIPMENT_LEVEL: () => EQUIPMENT_LEVEL,
|
|
46
|
+
EQUIPMENT_STATUS: () => EQUIPMENT_STATUS,
|
|
46
47
|
ERP_CAPABILITY: () => ERP_CAPABILITY,
|
|
47
48
|
ERP_MASTER_AXIS: () => ERP_MASTER_AXIS,
|
|
49
|
+
FIRST_PASS_RULE: () => FIRST_PASS_RULE,
|
|
48
50
|
GUARD_PRAGMA: () => GUARD_PRAGMA,
|
|
49
51
|
ILMD_ATTR: () => ILMD_ATTR,
|
|
50
52
|
LOCATION_SATURATION_NEAR: () => LOCATION_SATURATION_NEAR,
|
|
@@ -96,6 +98,8 @@ __export(index_exports, {
|
|
|
96
98
|
commandSpecGaps: () => commandSpecGaps,
|
|
97
99
|
commandTypeOf: () => commandTypeOf,
|
|
98
100
|
commandsOf: () => commandsOf,
|
|
101
|
+
computeFirstPassYield: () => computeFirstPassYield,
|
|
102
|
+
computeMtbf: () => computeMtbf,
|
|
99
103
|
computeOee: () => computeOee,
|
|
100
104
|
conversionFactorOf: () => conversionFactorOf,
|
|
101
105
|
criterionSaysNothing: () => criterionSaysNothing,
|
|
@@ -137,9 +141,12 @@ __export(index_exports, {
|
|
|
137
141
|
isEnergyUsagePeriodRecord: () => isEnergyUsagePeriodRecord,
|
|
138
142
|
isEpcisEventType: () => isEpcisEventType,
|
|
139
143
|
isEquipmentLevel: () => isEquipmentLevel,
|
|
144
|
+
isEquipmentStatus: () => isEquipmentStatus,
|
|
145
|
+
isFailureStatus: () => isFailureStatus,
|
|
140
146
|
isMasterDataRecord: () => isMasterDataRecord,
|
|
141
147
|
isOperationalRecord: () => isOperationalRecord,
|
|
142
148
|
isOrderTerminal: () => isOrderTerminal,
|
|
149
|
+
isPlannedStopStatus: () => isPlannedStopStatus,
|
|
143
150
|
isTransformationRecord: () => isTransformationRecord,
|
|
144
151
|
isoDurationHours: () => isoDurationHours,
|
|
145
152
|
itemKeyOf: () => itemKeyOf,
|
|
@@ -157,6 +164,7 @@ __export(index_exports, {
|
|
|
157
164
|
minuteOfDayAt: () => minuteOfDayAt,
|
|
158
165
|
missingCommandPayload: () => missingCommandPayload,
|
|
159
166
|
nextCommandState: () => nextCommandState,
|
|
167
|
+
normalizeEquipmentStatus: () => normalizeEquipmentStatus,
|
|
160
168
|
objectEvent: () => objectEvent,
|
|
161
169
|
objectUri: () => objectUri,
|
|
162
170
|
observationAt: () => observationAt,
|
|
@@ -184,6 +192,7 @@ __export(index_exports, {
|
|
|
184
192
|
resolveSubject: () => resolveSubject,
|
|
185
193
|
retiredVocabularyIn: () => retiredVocabularyIn,
|
|
186
194
|
reversalKey: () => reversalKey,
|
|
195
|
+
rolledThroughputYield: () => rolledThroughputYield,
|
|
187
196
|
sgtinClass: () => sgtinClass,
|
|
188
197
|
sgtinUri: () => sgtinUri,
|
|
189
198
|
ssccUri: () => ssccUri,
|
|
@@ -396,19 +405,19 @@ function hierarchyOf(s, levelOfType) {
|
|
|
396
405
|
}
|
|
397
406
|
for (const start of known) {
|
|
398
407
|
const seen = [start];
|
|
399
|
-
for (let
|
|
400
|
-
if (seen.includes(
|
|
401
|
-
seen.push(
|
|
402
|
-
if (!known.has(
|
|
408
|
+
for (let at2 = parent.get(start); at2 !== void 0; at2 = parent.get(at2)) {
|
|
409
|
+
if (seen.includes(at2)) throw new Error(`location hierarchy has a cycle: ${[...seen, at2].join(" \u2192 ")}`);
|
|
410
|
+
seen.push(at2);
|
|
411
|
+
if (!known.has(at2)) break;
|
|
403
412
|
}
|
|
404
413
|
}
|
|
405
414
|
const homes = /* @__PURE__ */ new Map();
|
|
406
415
|
for (const m of s.equipment ?? []) if (m.homeLocation) push(homes, m.homeLocation, m.id);
|
|
407
416
|
const ancestorsOf = (id) => {
|
|
408
417
|
const out = [];
|
|
409
|
-
for (let
|
|
410
|
-
out.push(
|
|
411
|
-
if (!known.has(
|
|
418
|
+
for (let at2 = parent.get(id); at2 !== void 0; at2 = parent.get(at2)) {
|
|
419
|
+
out.push(at2);
|
|
420
|
+
if (!known.has(at2)) break;
|
|
412
421
|
}
|
|
413
422
|
return out;
|
|
414
423
|
};
|
|
@@ -463,18 +472,18 @@ var DISPOSITION_DECISION = [
|
|
|
463
472
|
/** 아직 정하지 않았다 — 묶어 두고 쓰지 않는다. */
|
|
464
473
|
"hold"
|
|
465
474
|
];
|
|
466
|
-
function testPassedAt(r,
|
|
475
|
+
function testPassedAt(r, at2) {
|
|
467
476
|
if (r.result !== "pass") return false;
|
|
468
|
-
if (!r.expiresAt || !
|
|
469
|
-
return parsedMs(
|
|
477
|
+
if (!r.expiresAt || !at2) return r.result === "pass";
|
|
478
|
+
return parsedMs(at2) <= parsedMs(r.expiresAt);
|
|
470
479
|
}
|
|
471
|
-
function meetsTests(required, results,
|
|
480
|
+
function meetsTests(required, results, at2) {
|
|
472
481
|
if (!required.length) return true;
|
|
473
482
|
const bySpec = new Map((results ?? []).map((r) => [r.specId, r]));
|
|
474
483
|
for (const specId of required) {
|
|
475
484
|
const r = bySpec.get(specId);
|
|
476
485
|
if (!r) continue;
|
|
477
|
-
if (!testPassedAt(r,
|
|
486
|
+
if (!testPassedAt(r, at2)) return false;
|
|
478
487
|
}
|
|
479
488
|
return true;
|
|
480
489
|
}
|
|
@@ -523,9 +532,9 @@ function judgeAgainstSpec(spec, result) {
|
|
|
523
532
|
}
|
|
524
533
|
return allJudged ? "pass" : void 0;
|
|
525
534
|
}
|
|
526
|
-
function effectivityAt(p,
|
|
527
|
-
if (!p || !
|
|
528
|
-
const atMs = parsedMs(
|
|
535
|
+
function effectivityAt(p, at2, opts) {
|
|
536
|
+
if (!p || !at2) return void 0;
|
|
537
|
+
const atMs = parsedMs(at2);
|
|
529
538
|
if (!Number.isFinite(atMs)) return void 0;
|
|
530
539
|
const from = p.effectiveStart ? parsedMs(p.effectiveStart) : NaN;
|
|
531
540
|
if (Number.isFinite(from) && atMs < from) return "not-yet";
|
|
@@ -535,12 +544,12 @@ function effectivityAt(p, at, opts) {
|
|
|
535
544
|
}
|
|
536
545
|
var PARSED_MAX = 64;
|
|
537
546
|
var parsedCache = /* @__PURE__ */ new Map();
|
|
538
|
-
function parsedMs(
|
|
539
|
-
const hit = parsedCache.get(
|
|
547
|
+
function parsedMs(at2) {
|
|
548
|
+
const hit = parsedCache.get(at2);
|
|
540
549
|
if (hit !== void 0) return hit;
|
|
541
|
-
const ms = Date.parse(
|
|
550
|
+
const ms = Date.parse(at2);
|
|
542
551
|
if (parsedCache.size >= PARSED_MAX) parsedCache.clear();
|
|
543
|
-
parsedCache.set(
|
|
552
|
+
parsedCache.set(at2, ms);
|
|
544
553
|
return ms;
|
|
545
554
|
}
|
|
546
555
|
var classIndexCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -553,9 +562,9 @@ function classIndex(defs) {
|
|
|
553
562
|
return built;
|
|
554
563
|
}
|
|
555
564
|
var EMPTY_CLASS_INDEX = /* @__PURE__ */ new Map();
|
|
556
|
-
function classClosure(directIds, defs,
|
|
565
|
+
function classClosure(directIds, defs, at2) {
|
|
557
566
|
const byId = classIndex(defs);
|
|
558
|
-
const inWindow = (d) => !d || effectivityAt(d,
|
|
567
|
+
const inWindow = (d) => !d || effectivityAt(d, at2) === void 0;
|
|
559
568
|
const out = /* @__PURE__ */ new Set();
|
|
560
569
|
const stack = [...directIds ?? []];
|
|
561
570
|
while (stack.length) {
|
|
@@ -725,14 +734,14 @@ function offCalendarAt(r, ms, utcOffsetMinutes) {
|
|
|
725
734
|
const h = Math.floor(minute / 60);
|
|
726
735
|
return !(w.startHour <= w.endHour ? h >= w.startHour && h < w.endHour : h >= w.startHour || h < w.endHour);
|
|
727
736
|
}
|
|
728
|
-
function requiredTestsFor(directIds, defs,
|
|
737
|
+
function requiredTestsFor(directIds, defs, at2) {
|
|
729
738
|
if (!defs?.length) return [];
|
|
730
739
|
const cache = requiredTestsCache.get(defs) ?? /* @__PURE__ */ new Map();
|
|
731
740
|
if (!requiredTestsCache.has(defs)) requiredTestsCache.set(defs, cache);
|
|
732
|
-
const key = `${
|
|
741
|
+
const key = `${at2 ?? ""}\0${(directIds ?? []).join("")}`;
|
|
733
742
|
const hit = cache.get(key);
|
|
734
743
|
if (hit) return hit;
|
|
735
|
-
const closure = classClosure(directIds, defs,
|
|
744
|
+
const closure = classClosure(directIds, defs, at2);
|
|
736
745
|
const required = [];
|
|
737
746
|
for (const d of defs) if (closure.has(d.id)) required.push(...d.testSpecificationIds ?? []);
|
|
738
747
|
if (cache.size >= REQUIRED_TESTS_MAX) cache.clear();
|
|
@@ -742,27 +751,27 @@ function requiredTestsFor(directIds, defs, at) {
|
|
|
742
751
|
var REQUIRED_TESTS_MAX = 512;
|
|
743
752
|
var requiredTestsCache = /* @__PURE__ */ new WeakMap();
|
|
744
753
|
function capabilityOf(r, ctx) {
|
|
745
|
-
const
|
|
746
|
-
const eff = effectivityAt(r,
|
|
754
|
+
const at2 = ctx?.at;
|
|
755
|
+
const eff = effectivityAt(r, at2);
|
|
747
756
|
if (eff === "not-yet") return { available: false, reason: "not-yet" };
|
|
748
757
|
if (eff === "expired") return { available: false, reason: "retired" };
|
|
749
758
|
if (r.held) return { available: false, reason: "held" };
|
|
750
759
|
if (r.status === "down") return { available: false, reason: "down" };
|
|
751
|
-
if (
|
|
752
|
-
const ms = parsedMs(
|
|
760
|
+
if (at2) {
|
|
761
|
+
const ms = parsedMs(at2);
|
|
753
762
|
if (Number.isFinite(ms)) {
|
|
754
763
|
const why = offCalendarReasonAt(r, ms, ctx?.utcOffsetMinutes);
|
|
755
764
|
if (why === "non-working") return { available: false, reason: "resting" };
|
|
756
765
|
if (why === "off-hours") return { available: false, reason: "off-shift" };
|
|
757
766
|
}
|
|
758
767
|
}
|
|
759
|
-
if (ctx?.requiredTests?.length && !meetsTests(ctx.requiredTests, r.testResults,
|
|
768
|
+
if (ctx?.requiredTests?.length && !meetsTests(ctx.requiredTests, r.testResults, at2))
|
|
760
769
|
return { available: false, reason: "test-expired" };
|
|
761
770
|
if (r.status && r.status !== "idle" && r.status !== "available") return { available: false, reason: "working" };
|
|
762
771
|
return { available: true, reason: "available" };
|
|
763
772
|
}
|
|
764
|
-
function observationAt(observations, propertyId,
|
|
765
|
-
const t = parsedMs(
|
|
773
|
+
function observationAt(observations, propertyId, at2) {
|
|
774
|
+
const t = parsedMs(at2);
|
|
766
775
|
if (!(t >= 0)) return void 0;
|
|
767
776
|
let best;
|
|
768
777
|
let bestStart = -1;
|
|
@@ -976,6 +985,20 @@ var ENERGY_EVENT = {
|
|
|
976
985
|
drSuggested: "energy.dr.suggested"
|
|
977
986
|
};
|
|
978
987
|
var OBSERVATION_BASIS = ["metered", "provider", "billed", "estimated"];
|
|
988
|
+
var EQUIPMENT_STATUS = ["busy", "setup", "down", "idle", "planned-stop"];
|
|
989
|
+
function isEquipmentStatus(value) {
|
|
990
|
+
return typeof value === "string" && EQUIPMENT_STATUS.includes(value);
|
|
991
|
+
}
|
|
992
|
+
function normalizeEquipmentStatus(value) {
|
|
993
|
+
const said = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
994
|
+
return isEquipmentStatus(said) ? said : void 0;
|
|
995
|
+
}
|
|
996
|
+
function isFailureStatus(value) {
|
|
997
|
+
return normalizeEquipmentStatus(value) === "down";
|
|
998
|
+
}
|
|
999
|
+
function isPlannedStopStatus(value) {
|
|
1000
|
+
return normalizeEquipmentStatus(value) === "planned-stop";
|
|
1001
|
+
}
|
|
979
1002
|
var CMD = {
|
|
980
1003
|
orderHold: "order.hold",
|
|
981
1004
|
orderResume: "order.resume",
|
|
@@ -2044,8 +2067,8 @@ function ingestEnergyRecords(records, opts) {
|
|
|
2044
2067
|
const errors = [];
|
|
2045
2068
|
const meterId = String(record?.meterId ?? "").trim();
|
|
2046
2069
|
if (!meterId) errors.push("meterId \uC5C6\uC74C \u2014 \uC5B4\uB514\uC758 \uC18C\uBE44\uC778\uC9C0 \uBAA8\uB974\uB294 \uAC12\uC740 \uB204\uC801\uD560 \uC218 \uC5C6\uB2E4");
|
|
2047
|
-
const
|
|
2048
|
-
const atMs =
|
|
2070
|
+
const at2 = String(record?.at ?? "").trim() || opts.defaultEventTime;
|
|
2071
|
+
const atMs = at2 ? Date.parse(at2) : Number.NaN;
|
|
2049
2072
|
if (!Number.isFinite(atMs)) errors.push("at \uC5C6\uC74C/\uD615\uC2DD \uC624\uB958 \u2014 \uC9C0\uAE08 \uC2DC\uAC01\uC73C\uB85C \uBA54\uC6B0\uBA74 \uB0A8\uC758 \uC218\uC694 \uAD6C\uAC04\uC5D0 \uC2E4\uB9B0\uB2E4");
|
|
2050
2073
|
const num = (v, name) => {
|
|
2051
2074
|
if (v === void 0 || v === null || v === "") return void 0;
|
|
@@ -2100,8 +2123,8 @@ function ingestEnergyEquipmentRecords(records, opts) {
|
|
|
2100
2123
|
const r = record;
|
|
2101
2124
|
const equipmentId = String(r?.equipmentId ?? "").trim();
|
|
2102
2125
|
if (!equipmentId) errors.push("equipmentId \uC5C6\uC74C \u2014 \uC5B4\uB290 \uC124\uBE44\uC758 \uC0C1\uD0DC\uC778\uC9C0 \uBAA8\uB974\uB294 \uAC12\uC740 \uC2E4\uC744 \uC218 \uC5C6\uB2E4");
|
|
2103
|
-
const
|
|
2104
|
-
const atMs =
|
|
2126
|
+
const at2 = String(r?.at ?? "").trim() || opts.defaultEventTime;
|
|
2127
|
+
const atMs = at2 ? Date.parse(at2) : Number.NaN;
|
|
2105
2128
|
if (!Number.isFinite(atMs)) errors.push("at \uC5C6\uC74C/\uD615\uC2DD \uC624\uB958 \u2014 \uC9C0\uAE08 \uC2DC\uAC01\uC73C\uB85C \uBA54\uC6B0\uBA74 \uC5B8\uC81C\uC758 \uC0C1\uD0DC\uC778\uC9C0 \uC54C \uC218 \uC5C6\uB2E4");
|
|
2106
2129
|
const num = (v, name, min, max) => {
|
|
2107
2130
|
if (v === void 0 || v === null || v === "") return void 0;
|
|
@@ -2239,9 +2262,9 @@ function ingestEnergyGenerationRecords(records, opts) {
|
|
|
2239
2262
|
const rawSince = r?.kWhSince;
|
|
2240
2263
|
if (rawSince !== void 0 && rawSince !== null && String(rawSince).trim()) {
|
|
2241
2264
|
const text = String(rawSince).trim();
|
|
2242
|
-
const
|
|
2243
|
-
if (!Number.isFinite(
|
|
2244
|
-
else if (Number.isFinite(Date.parse(eventTime)) &&
|
|
2265
|
+
const at2 = Date.parse(text);
|
|
2266
|
+
if (!Number.isFinite(at2)) errors.push(`kWhSince \uB97C \uC2DC\uAC01\uC73C\uB85C \uC77D\uC744 \uC218 \uC5C6\uB2E4: ${JSON.stringify(rawSince)}`);
|
|
2267
|
+
else if (Number.isFinite(Date.parse(eventTime)) && at2 > Date.parse(eventTime)) {
|
|
2245
2268
|
errors.push(`kWhSince(${text}) \uAC00 \uC7B0 \uC2DC\uAC01(${eventTime})\uBCF4\uB2E4 \uB4A4\uB2E4 \u2014 \uB458 \uC911 \uD558\uB098\uAC00 \uD2C0\uB838\uB2E4`);
|
|
2246
2269
|
} else since = text;
|
|
2247
2270
|
}
|
|
@@ -3196,13 +3219,13 @@ function lotFromAttributes(attrs) {
|
|
|
3196
3219
|
function readEpochMs(raw) {
|
|
3197
3220
|
if (typeof raw === "number") return Number.isFinite(raw) ? raw : void 0;
|
|
3198
3221
|
if (typeof raw !== "string" || !raw.trim()) return void 0;
|
|
3199
|
-
const
|
|
3200
|
-
return Number.isNaN(
|
|
3222
|
+
const at2 = Date.parse(raw);
|
|
3223
|
+
return Number.isNaN(at2) ? void 0 : at2;
|
|
3201
3224
|
}
|
|
3202
3225
|
|
|
3203
3226
|
// src/operational-ingest.ts
|
|
3204
3227
|
var TASK_STATUS = ["created", "assigned", "in-progress", "completed"];
|
|
3205
|
-
var
|
|
3228
|
+
var EQUIPMENT_STATUS2 = ["idle", "busy", "down", "setup", "planned-stop"];
|
|
3206
3229
|
var PERSON_STATUS = ["idle", "busy"];
|
|
3207
3230
|
var ASSET_STATUS = ["idle", "in-use"];
|
|
3208
3231
|
var SPECS = {
|
|
@@ -3270,7 +3293,7 @@ var SPECS = {
|
|
|
3270
3293
|
재검사하지는 않는다: 그 모양은 `EquipmentMotion` 계약이고, 여기서 두 번 지키면 두 벌이 된다. */
|
|
3271
3294
|
motion: "object"
|
|
3272
3295
|
},
|
|
3273
|
-
enums: { status:
|
|
3296
|
+
enums: { status: EQUIPMENT_STATUS2 }
|
|
3274
3297
|
},
|
|
3275
3298
|
person: {
|
|
3276
3299
|
eventType: OP_EVENT.person,
|
|
@@ -3415,7 +3438,7 @@ var SPECS = {
|
|
|
3415
3438
|
decidedBy: "string",
|
|
3416
3439
|
recordTime: "string"
|
|
3417
3440
|
},
|
|
3418
|
-
enums: { status:
|
|
3441
|
+
enums: { status: EQUIPMENT_STATUS2 }
|
|
3419
3442
|
},
|
|
3420
3443
|
/*
|
|
3421
3444
|
* **부적합 처분** — 재고 판정을 받은 것을 어떻게 하기로 정했나.
|
|
@@ -3633,8 +3656,8 @@ function ingestOperationalRecords(records, opts) {
|
|
|
3633
3656
|
}
|
|
3634
3657
|
}
|
|
3635
3658
|
const closedPeriodEnd = kind === "equipment-period" ? String(r.to ?? "").trim() : "";
|
|
3636
|
-
const
|
|
3637
|
-
const atMs =
|
|
3659
|
+
const at2 = closedPeriodEnd || String(r.at ?? "").trim() || opts.defaultEventTime;
|
|
3660
|
+
const atMs = at2 ? Date.parse(at2) : Number.NaN;
|
|
3638
3661
|
if (!Number.isFinite(atMs)) {
|
|
3639
3662
|
errors.push(`${kind}: at \uC5C6\uC74C/\uD615\uC2DD \uC624\uB958 \u2014 \uC2DC\uAC01 \uC5C6\uC774\uB294 \uB2A6\uAC8C \uC628 \uC61B \uC0AC\uC2E4\uC744 \uAC78\uB7EC\uB0BC \uC218 \uC5C6\uB2E4`);
|
|
3640
3663
|
}
|
|
@@ -3726,28 +3749,28 @@ function operationsCapabilityOf(input) {
|
|
|
3726
3749
|
var DISTRIBUTIONS = /* @__PURE__ */ new Set(["poisson", "uniform", "constant", "profile"]);
|
|
3727
3750
|
var isNum = (v) => typeof v === "number" && Number.isFinite(v);
|
|
3728
3751
|
var bad = (errorCode, errorParams) => ({ ok: false, errorCode, errorParams });
|
|
3729
|
-
function validateGenerator(g,
|
|
3730
|
-
if (!g || typeof g !== "object") return bad("scenario-generator-invalid", { at });
|
|
3731
|
-
if (!g.kind || typeof g.kind !== "string") return bad("scenario-generator-kind-required", { at });
|
|
3752
|
+
function validateGenerator(g, at2) {
|
|
3753
|
+
if (!g || typeof g !== "object") return bad("scenario-generator-invalid", { at: at2 });
|
|
3754
|
+
if (!g.kind || typeof g.kind !== "string") return bad("scenario-generator-kind-required", { at: at2 });
|
|
3732
3755
|
const rate = g.rate;
|
|
3733
|
-
if (!rate || typeof rate !== "object") return bad("scenario-rate-required", { at, kind: g.kind });
|
|
3734
|
-
if (!isNum(rate.meanPerHour) || rate.meanPerHour < 0) return bad("scenario-rate-mean-invalid", { at, kind: g.kind });
|
|
3735
|
-
if (!DISTRIBUTIONS.has(rate.distribution)) return bad("scenario-rate-distribution-invalid", { at, kind: g.kind, distribution: String(rate.distribution ?? "") });
|
|
3736
|
-
if (rate.distribution === "profile" && !Array.isArray(rate.profile)) return bad("scenario-rate-profile-required", { at, kind: g.kind });
|
|
3756
|
+
if (!rate || typeof rate !== "object") return bad("scenario-rate-required", { at: at2, kind: g.kind });
|
|
3757
|
+
if (!isNum(rate.meanPerHour) || rate.meanPerHour < 0) return bad("scenario-rate-mean-invalid", { at: at2, kind: g.kind });
|
|
3758
|
+
if (!DISTRIBUTIONS.has(rate.distribution)) return bad("scenario-rate-distribution-invalid", { at: at2, kind: g.kind, distribution: String(rate.distribution ?? "") });
|
|
3759
|
+
if (rate.distribution === "profile" && !Array.isArray(rate.profile)) return bad("scenario-rate-profile-required", { at: at2, kind: g.kind });
|
|
3737
3760
|
const content = g.content;
|
|
3738
|
-
if (!content || typeof content !== "object") return bad("scenario-content-required", { at, kind: g.kind });
|
|
3739
|
-
if (!Array.isArray(content.skuMix) || content.skuMix.length === 0) return bad("scenario-sku-mix-required", { at, kind: g.kind });
|
|
3761
|
+
if (!content || typeof content !== "object") return bad("scenario-content-required", { at: at2, kind: g.kind });
|
|
3762
|
+
if (!Array.isArray(content.skuMix) || content.skuMix.length === 0) return bad("scenario-sku-mix-required", { at: at2, kind: g.kind });
|
|
3740
3763
|
for (const s of content.skuMix) {
|
|
3741
|
-
if (!s?.gtin || typeof s.gtin !== "string") return bad("scenario-sku-gtin-required", { at, kind: g.kind });
|
|
3742
|
-
if (!isNum(s.weight) || s.weight <= 0) return bad("scenario-sku-weight-invalid", { at, kind: g.kind, gtin: String(s.gtin) });
|
|
3764
|
+
if (!s?.gtin || typeof s.gtin !== "string") return bad("scenario-sku-gtin-required", { at: at2, kind: g.kind });
|
|
3765
|
+
if (!isNum(s.weight) || s.weight <= 0) return bad("scenario-sku-weight-invalid", { at: at2, kind: g.kind, gtin: String(s.gtin) });
|
|
3743
3766
|
}
|
|
3744
3767
|
const q = content.qtyPerLine;
|
|
3745
|
-
if (!q || !isNum(q.min) || !isNum(q.max)) return bad("scenario-qty-required", { at, kind: g.kind });
|
|
3746
|
-
if (q.min < 0 || q.max < q.min) return bad("scenario-qty-range-invalid", { at, kind: g.kind, min: q.min, max: q.max });
|
|
3768
|
+
if (!q || !isNum(q.min) || !isNum(q.max)) return bad("scenario-qty-required", { at: at2, kind: g.kind });
|
|
3769
|
+
if (q.min < 0 || q.max < q.min) return bad("scenario-qty-range-invalid", { at: at2, kind: g.kind, min: q.min, max: q.max });
|
|
3747
3770
|
const l = content.linesPerOrder;
|
|
3748
|
-
if (l && (!isNum(l.min) || !isNum(l.max) || l.min < 1 || l.max < l.min)) return bad("scenario-lines-range-invalid", { at, kind: g.kind });
|
|
3771
|
+
if (l && (!isNum(l.min) || !isNum(l.max) || l.min < 1 || l.max < l.min)) return bad("scenario-lines-range-invalid", { at: at2, kind: g.kind });
|
|
3749
3772
|
if (g.stimulus !== void 0 && g.stimulus !== "arrival" && g.stimulus !== "order") {
|
|
3750
|
-
return bad("scenario-stimulus-invalid", { at, kind: g.kind, stimulus: String(g.stimulus) });
|
|
3773
|
+
return bad("scenario-stimulus-invalid", { at: at2, kind: g.kind, stimulus: String(g.stimulus) });
|
|
3751
3774
|
}
|
|
3752
3775
|
return { ok: true };
|
|
3753
3776
|
}
|
|
@@ -3893,6 +3916,98 @@ function computeOee(c, nowMs) {
|
|
|
3893
3916
|
};
|
|
3894
3917
|
}
|
|
3895
3918
|
|
|
3919
|
+
// src/yield.ts
|
|
3920
|
+
var FIRST_PASS_RULE = "\uC7AC\uC791\uC5C5\xB7\uC218\uB9AC\uB85C \uD1B5\uACFC\uD55C \uC0B0\uCD9C\uC740 \uC6D0\uB798 \uACF5\uC815 \uC2E4\uC801\uC758 \uC591\uD488\uC5D0 \uB418\uB354\uD558\uC9C0 \uC54A\uB294\uB2E4 \u2014 \uADF8\uAC83\uC744 \uB354\uD558\uBA74 \uC774 \uC218\uB294 \uD488\uC9C8\uB960\uC774 \uB41C\uB2E4";
|
|
3921
|
+
var finite = (n) => typeof n === "number" && Number.isFinite(n);
|
|
3922
|
+
function computeFirstPassYield(input) {
|
|
3923
|
+
const missing = [];
|
|
3924
|
+
const { jobOrderName, operationKey } = input;
|
|
3925
|
+
const good = input.good;
|
|
3926
|
+
const bad2 = input.nonconforming;
|
|
3927
|
+
const known = input.entered !== void 0;
|
|
3928
|
+
if (!finite(good) || !finite(bad2) || good < 0 || bad2 < 0 || known && (!finite(input.entered) || input.entered < 0)) {
|
|
3929
|
+
return { jobOrderName, operationKey, firstPassGood: 0, completed: 0, basis: "completed", missing: ["invalid-counts"] };
|
|
3930
|
+
}
|
|
3931
|
+
const completed = good + bad2;
|
|
3932
|
+
if (completed <= 0) missing.push("no-input");
|
|
3933
|
+
if (known && completed > input.entered) missing.push("inconsistent-counts");
|
|
3934
|
+
const inProcess = known ? Math.max(0, input.entered - completed) : void 0;
|
|
3935
|
+
if (missing.length) {
|
|
3936
|
+
return {
|
|
3937
|
+
jobOrderName,
|
|
3938
|
+
operationKey,
|
|
3939
|
+
firstPassGood: good,
|
|
3940
|
+
completed,
|
|
3941
|
+
basis: "completed",
|
|
3942
|
+
...inProcess === void 0 ? {} : { inProcess },
|
|
3943
|
+
missing
|
|
3944
|
+
};
|
|
3945
|
+
}
|
|
3946
|
+
return {
|
|
3947
|
+
jobOrderName,
|
|
3948
|
+
operationKey,
|
|
3949
|
+
fpy: good / completed,
|
|
3950
|
+
firstPassGood: good,
|
|
3951
|
+
completed,
|
|
3952
|
+
basis: "completed",
|
|
3953
|
+
...inProcess === void 0 ? {} : { inProcess },
|
|
3954
|
+
missing
|
|
3955
|
+
};
|
|
3956
|
+
}
|
|
3957
|
+
function rolledThroughputYield(steps) {
|
|
3958
|
+
const usable = steps.filter((s) => s.fpy !== void 0);
|
|
3959
|
+
const skipped = steps.length - usable.length;
|
|
3960
|
+
if (!usable.length || skipped > 0) return { countedSteps: usable.length, skipped };
|
|
3961
|
+
return { rty: usable.reduce((acc, s) => acc * s.fpy, 1), countedSteps: usable.length, skipped };
|
|
3962
|
+
}
|
|
3963
|
+
|
|
3964
|
+
// src/reliability.ts
|
|
3965
|
+
var at = (t) => typeof t === "string" ? Date.parse(t) : NaN;
|
|
3966
|
+
function computeMtbf(input) {
|
|
3967
|
+
const { equipmentId } = input;
|
|
3968
|
+
const missing = [];
|
|
3969
|
+
const unknown = /* @__PURE__ */ new Set();
|
|
3970
|
+
const sorted = [...input.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);
|
|
3971
|
+
let operatingMs = 0;
|
|
3972
|
+
let failures = 0;
|
|
3973
|
+
let downPeriods = 0;
|
|
3974
|
+
let invalid = false;
|
|
3975
|
+
let wasDown = false;
|
|
3976
|
+
for (const p of sorted) {
|
|
3977
|
+
if (!Number.isFinite(p.from) || !Number.isFinite(p.to) || p.to <= p.from) {
|
|
3978
|
+
invalid = true;
|
|
3979
|
+
continue;
|
|
3980
|
+
}
|
|
3981
|
+
if (p.status === void 0) {
|
|
3982
|
+
unknown.add(p.said);
|
|
3983
|
+
wasDown = false;
|
|
3984
|
+
continue;
|
|
3985
|
+
}
|
|
3986
|
+
if (p.status === "busy") operatingMs += p.to - p.from;
|
|
3987
|
+
if (p.status === "down") {
|
|
3988
|
+
downPeriods++;
|
|
3989
|
+
if (!wasDown) failures++;
|
|
3990
|
+
wasDown = true;
|
|
3991
|
+
} else {
|
|
3992
|
+
wasDown = false;
|
|
3993
|
+
}
|
|
3994
|
+
}
|
|
3995
|
+
if (invalid) missing.push("invalid-period");
|
|
3996
|
+
if (unknown.size) missing.push("unknown-status");
|
|
3997
|
+
if (operatingMs <= 0) missing.push("no-operating-time");
|
|
3998
|
+
if (failures === 0) missing.push("no-failure");
|
|
3999
|
+
const out = {
|
|
4000
|
+
equipmentId,
|
|
4001
|
+
operatingMs,
|
|
4002
|
+
failures,
|
|
4003
|
+
downPeriods,
|
|
4004
|
+
unknownStatuses: [...unknown],
|
|
4005
|
+
missing
|
|
4006
|
+
};
|
|
4007
|
+
if (missing.length) return out;
|
|
4008
|
+
return { ...out, mtbfMs: operatingMs / failures };
|
|
4009
|
+
}
|
|
4010
|
+
|
|
3896
4011
|
// src/erp.ts
|
|
3897
4012
|
var SCHEDULE_STATUS = ["draft", "released", "started", "completed", "closed", "cancelled"];
|
|
3898
4013
|
function acceptsPerformance(status) {
|
|
@@ -4078,8 +4193,10 @@ function commandSpecGaps(specs, command) {
|
|
|
4078
4193
|
EPCIS_CONTEXT,
|
|
4079
4194
|
EPCIS_EVENT,
|
|
4080
4195
|
EQUIPMENT_LEVEL,
|
|
4196
|
+
EQUIPMENT_STATUS,
|
|
4081
4197
|
ERP_CAPABILITY,
|
|
4082
4198
|
ERP_MASTER_AXIS,
|
|
4199
|
+
FIRST_PASS_RULE,
|
|
4083
4200
|
GUARD_PRAGMA,
|
|
4084
4201
|
ILMD_ATTR,
|
|
4085
4202
|
LOCATION_SATURATION_NEAR,
|
|
@@ -4131,6 +4248,8 @@ function commandSpecGaps(specs, command) {
|
|
|
4131
4248
|
commandSpecGaps,
|
|
4132
4249
|
commandTypeOf,
|
|
4133
4250
|
commandsOf,
|
|
4251
|
+
computeFirstPassYield,
|
|
4252
|
+
computeMtbf,
|
|
4134
4253
|
computeOee,
|
|
4135
4254
|
conversionFactorOf,
|
|
4136
4255
|
criterionSaysNothing,
|
|
@@ -4172,9 +4291,12 @@ function commandSpecGaps(specs, command) {
|
|
|
4172
4291
|
isEnergyUsagePeriodRecord,
|
|
4173
4292
|
isEpcisEventType,
|
|
4174
4293
|
isEquipmentLevel,
|
|
4294
|
+
isEquipmentStatus,
|
|
4295
|
+
isFailureStatus,
|
|
4175
4296
|
isMasterDataRecord,
|
|
4176
4297
|
isOperationalRecord,
|
|
4177
4298
|
isOrderTerminal,
|
|
4299
|
+
isPlannedStopStatus,
|
|
4178
4300
|
isTransformationRecord,
|
|
4179
4301
|
isoDurationHours,
|
|
4180
4302
|
itemKeyOf,
|
|
@@ -4192,6 +4314,7 @@ function commandSpecGaps(specs, command) {
|
|
|
4192
4314
|
minuteOfDayAt,
|
|
4193
4315
|
missingCommandPayload,
|
|
4194
4316
|
nextCommandState,
|
|
4317
|
+
normalizeEquipmentStatus,
|
|
4195
4318
|
objectEvent,
|
|
4196
4319
|
objectUri,
|
|
4197
4320
|
observationAt,
|
|
@@ -4219,6 +4342,7 @@ function commandSpecGaps(specs, command) {
|
|
|
4219
4342
|
resolveSubject,
|
|
4220
4343
|
retiredVocabularyIn,
|
|
4221
4344
|
reversalKey,
|
|
4345
|
+
rolledThroughputYield,
|
|
4222
4346
|
sgtinClass,
|
|
4223
4347
|
sgtinUri,
|
|
4224
4348
|
ssccUri,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@operato/ops-contract",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
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",
|