@operato/ops-contract 0.9.4 → 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.
@@ -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
  *
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
  *
@@ -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 =
@@ -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`). 해소는 호스트가 한다. */
@@ -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;
@@ -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
- export function computeMtbf(input) {
46
- const { equipmentId } = input;
47
- const missing = [];
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 = [...(input.periods ?? [])]
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
- if (invalid)
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 (unknown.size)
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
- operatingMs,
97
- failures,
98
- downPeriods,
99
- unknownStatuses: [...unknown],
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, mtbfMs: operatingMs / failures };
153
+ return { ...out, mttrMs: s.downMs / s.failures };
105
154
  }
@@ -62,6 +62,7 @@ __export(index_exports, {
62
62
  OP_PARAM: () => OP_PARAM,
63
63
  ORDER_TERMINAL_STATUS: () => ORDER_TERMINAL_STATUS,
64
64
  PRIORITY_UNSET: () => PRIORITY_UNSET,
65
+ RESOURCE_KIND: () => RESOURCE_KIND,
65
66
  RETIRED_VOCABULARY: () => RETIRED_VOCABULARY,
66
67
  SCHEDULE_STATUS: () => SCHEDULE_STATUS,
67
68
  TWIN_AXES: () => TWIN_AXES,
@@ -103,6 +104,7 @@ __export(index_exports, {
103
104
  commandsOf: () => commandsOf,
104
105
  computeFirstPassYield: () => computeFirstPassYield,
105
106
  computeMtbf: () => computeMtbf,
107
+ computeMttr: () => computeMttr,
106
108
  computeOee: () => computeOee,
107
109
  conversionFactorOf: () => conversionFactorOf,
108
110
  criterionSaysNothing: () => criterionSaysNothing,
@@ -151,6 +153,7 @@ __export(index_exports, {
151
153
  isOperationalRecord: () => isOperationalRecord,
152
154
  isOrderTerminal: () => isOrderTerminal,
153
155
  isPlannedStopStatus: () => isPlannedStopStatus,
156
+ isResourceKind: () => isResourceKind,
154
157
  isTransformationRecord: () => isTransformationRecord,
155
158
  isoDurationHours: () => isoDurationHours,
156
159
  itemKeyOf: () => itemKeyOf,
@@ -203,6 +206,7 @@ __export(index_exports, {
203
206
  ssccUri: () => ssccUri,
204
207
  stateFieldsOf: () => stateFieldsOf,
205
208
  subLotIdOf: () => subLotIdOf,
209
+ tenseOf: () => tenseOf,
206
210
  testEvidenceGaps: () => testEvidenceGaps,
207
211
  testPassedAt: () => testPassedAt,
208
212
  transactionEvent: () => transactionEvent,
@@ -1024,6 +1028,10 @@ var EQUIPMENT_STATUS = ["busy", "setup", "down", "idle", "planned-stop"];
1024
1028
  function isEquipmentStatus(value) {
1025
1029
  return typeof value === "string" && EQUIPMENT_STATUS.includes(value);
1026
1030
  }
1031
+ var RESOURCE_KIND = ["equipment", "personnel", "material", "asset"];
1032
+ function isResourceKind(value) {
1033
+ return typeof value === "string" && RESOURCE_KIND.includes(value);
1034
+ }
1027
1035
  function normalizeEquipmentStatus(value) {
1028
1036
  const said = typeof value === "string" ? value.trim().toLowerCase() : "";
1029
1037
  return isEquipmentStatus(said) ? said : void 0;
@@ -1860,6 +1868,9 @@ var TWIN_AXES = [
1860
1868
  systems: ["ems"]
1861
1869
  }
1862
1870
  ];
1871
+ function tenseOf(r) {
1872
+ return r?.tense;
1873
+ }
1863
1874
  var TWIN_RELATIONS = [
1864
1875
  /* 자리가 속한 구역 — **board 밖**(호스트의 `TwinArea`). 해소는 호스트가 한다. */
1865
1876
  { from: "locations", field: "parentId", target: { kind: "external", entity: "space.area" }, via: "twin.rel.area", optional: true },
@@ -4070,12 +4081,11 @@ function rolledThroughputYield(steps) {
4070
4081
 
4071
4082
  // src/reliability.ts
4072
4083
  var at = (t) => typeof t === "string" ? Date.parse(t) : NaN;
4073
- function computeMtbf(input) {
4074
- const { equipmentId } = input;
4075
- const missing = [];
4084
+ function scanPeriods(periods) {
4076
4085
  const unknown = /* @__PURE__ */ new Set();
4077
- 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);
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);
4078
4087
  let operatingMs = 0;
4088
+ let downMs = 0;
4079
4089
  let failures = 0;
4080
4090
  let downPeriods = 0;
4081
4091
  let invalid = false;
@@ -4092,6 +4102,7 @@ function computeMtbf(input) {
4092
4102
  }
4093
4103
  if (p.status === "busy") operatingMs += p.to - p.from;
4094
4104
  if (p.status === "down") {
4105
+ downMs += p.to - p.from;
4095
4106
  downPeriods++;
4096
4107
  if (!wasDown) failures++;
4097
4108
  wasDown = true;
@@ -4099,20 +4110,42 @@ function computeMtbf(input) {
4099
4110
  wasDown = false;
4100
4111
  }
4101
4112
  }
4102
- if (invalid) missing.push("invalid-period");
4103
- if (unknown.size) missing.push("unknown-status");
4104
- if (operatingMs <= 0) missing.push("no-operating-time");
4105
- if (failures === 0) missing.push("no-failure");
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");
4106
4139
  const out = {
4107
- equipmentId,
4108
- operatingMs,
4109
- failures,
4110
- downPeriods,
4111
- unknownStatuses: [...unknown],
4140
+ equipmentId: input.equipmentId,
4141
+ downMs: s.downMs,
4142
+ failures: s.failures,
4143
+ downPeriods: s.downPeriods,
4144
+ unknownStatuses: s.unknownStatuses,
4112
4145
  missing
4113
4146
  };
4114
4147
  if (missing.length) return out;
4115
- return { ...out, mtbfMs: operatingMs / failures };
4148
+ return { ...out, mttrMs: s.downMs / s.failures };
4116
4149
  }
4117
4150
 
4118
4151
  // src/erp.ts
@@ -4319,6 +4352,7 @@ function commandSpecGaps(specs, command) {
4319
4352
  OP_PARAM,
4320
4353
  ORDER_TERMINAL_STATUS,
4321
4354
  PRIORITY_UNSET,
4355
+ RESOURCE_KIND,
4322
4356
  RETIRED_VOCABULARY,
4323
4357
  SCHEDULE_STATUS,
4324
4358
  TWIN_AXES,
@@ -4360,6 +4394,7 @@ function commandSpecGaps(specs, command) {
4360
4394
  commandsOf,
4361
4395
  computeFirstPassYield,
4362
4396
  computeMtbf,
4397
+ computeMttr,
4363
4398
  computeOee,
4364
4399
  conversionFactorOf,
4365
4400
  criterionSaysNothing,
@@ -4408,6 +4443,7 @@ function commandSpecGaps(specs, command) {
4408
4443
  isOperationalRecord,
4409
4444
  isOrderTerminal,
4410
4445
  isPlannedStopStatus,
4446
+ isResourceKind,
4411
4447
  isTransformationRecord,
4412
4448
  isoDurationHours,
4413
4449
  itemKeyOf,
@@ -4460,6 +4496,7 @@ function commandSpecGaps(specs, command) {
4460
4496
  ssccUri,
4461
4497
  stateFieldsOf,
4462
4498
  subLotIdOf,
4499
+ tenseOf,
4463
4500
  testEvidenceGaps,
4464
4501
  testPassedAt,
4465
4502
  transactionEvent,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/ops-contract",
3
- "version": "0.9.4",
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",