@operato/ops-contract 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/contract.d.ts +34 -5
- package/dist/domain-definition.d.ts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/oee.d.ts +54 -0
- package/dist/oee.js +62 -0
- package/dist/operational-ingest.js +46 -58
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist-cjs/index.cjs +67 -12
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -1379,17 +1379,43 @@ export interface EquipmentMotion {
|
|
|
1379
1379
|
* → OEE=(run/planned)×quality. 세 인자가 손실 위치(셋업·기아·불량)를 분해해 드러낸다.
|
|
1380
1380
|
*/
|
|
1381
1381
|
export interface OeeMetrics {
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1382
|
+
/**
|
|
1383
|
+
* ── 세 비율은 **없을 수 있다** (2026-08-30) ──────────────────────────────
|
|
1384
|
+
*
|
|
1385
|
+
* 예전에는 근거가 없을 때 `1` 을 냈다. 잰 구간이 없는데 「가동률 100%」가 되고, 만든 것이 하나도
|
|
1386
|
+
* 없는데 「양품률 100%」가 된다. **없는 근거로 좋은 숫자를 만드는 것**이고, 사용자는 그것으로
|
|
1387
|
+
* 판단한다.
|
|
1388
|
+
*
|
|
1389
|
+
* 없으면 답하지 않는다. 무엇이 없어서인지는 `missing` 이 말한다.
|
|
1390
|
+
*/
|
|
1391
|
+
availability?: number;
|
|
1392
|
+
performance?: number;
|
|
1393
|
+
quality?: number;
|
|
1394
|
+
overall?: number;
|
|
1395
|
+
/** 값이 없는 이유 — 비면 셋 다 나왔다는 뜻이다. */
|
|
1396
|
+
missing?: OeeMissing[];
|
|
1386
1397
|
runMs: number;
|
|
1387
1398
|
setupMs: number;
|
|
1388
1399
|
downMs: number;
|
|
1389
|
-
|
|
1400
|
+
delayMs: number;
|
|
1390
1401
|
goodCount: number;
|
|
1391
1402
|
scrapCount: number;
|
|
1392
1403
|
}
|
|
1404
|
+
/**
|
|
1405
|
+
* OEE 값이 없는 이유.
|
|
1406
|
+
*
|
|
1407
|
+
* 「모름」과 「0」은 다르다. 계획 조업 시간을 모르면 가동률을 낼 수 없고, 그때 0 을 내면 설비가
|
|
1408
|
+
* 놀고 있는 것처럼 보이고 1 을 내면 완벽한 것처럼 보인다. 둘 다 거짓이다.
|
|
1409
|
+
*/
|
|
1410
|
+
export type OeeMissing =
|
|
1411
|
+
/** 계획 조업 시간(PBT)을 모른다 — 아직 아무것도 재지 않았거나 근무 캘린더가 없다. */
|
|
1412
|
+
'planned-busy-time'
|
|
1413
|
+
/** 품목당 표준 시간(PRI)이 선언되지 않았다 — 성능률의 분자를 만들 수 없다. */
|
|
1414
|
+
| 'planned-run-time-per-item'
|
|
1415
|
+
/** 만든 것이 없다 — 양품률을 낼 대상이 없다. */
|
|
1416
|
+
| 'produced-quantity'
|
|
1417
|
+
/** 실 생산 시간이 0 이다 — 성능률의 분모가 없다. */
|
|
1418
|
+
| 'actual-production-time';
|
|
1393
1419
|
export interface EquipmentState extends EffectivePeriod {
|
|
1394
1420
|
id: string;
|
|
1395
1421
|
kind: string;
|
|
@@ -3230,6 +3256,8 @@ export interface TwinModelDef {
|
|
|
3230
3256
|
/** parallelism = 동시 처리 수(LocationState.parallelism 참조). capacity 는 저장 용량. */
|
|
3231
3257
|
locations: (EffectivePeriod & {
|
|
3232
3258
|
id: string;
|
|
3259
|
+
name?: string;
|
|
3260
|
+
gs1Id?: string;
|
|
3233
3261
|
type: string;
|
|
3234
3262
|
capacity: number;
|
|
3235
3263
|
parallelism?: number;
|
|
@@ -3243,6 +3271,7 @@ export interface TwinModelDef {
|
|
|
3243
3271
|
*/
|
|
3244
3272
|
equipment: (EffectivePeriod & {
|
|
3245
3273
|
id: string;
|
|
3274
|
+
name?: string;
|
|
3246
3275
|
kind: string;
|
|
3247
3276
|
homeLocation: string;
|
|
3248
3277
|
identity?: string;
|
|
@@ -133,6 +133,7 @@ export interface DurationVariability {
|
|
|
133
133
|
export interface OperationDef extends EffectivePeriod {
|
|
134
134
|
key: string;
|
|
135
135
|
label: string;
|
|
136
|
+
name?: string;
|
|
136
137
|
intent: OperationIntent;
|
|
137
138
|
/** 오퍼레이션이 수행되는 자리 타입(LocationTypeDef.key). 커널이 locationByType 로 위치 해소. */
|
|
138
139
|
locationType?: string;
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/oee.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { OeeMetrics } from './contract.ts';
|
|
2
|
+
/**
|
|
3
|
+
* OEE 계측 카운터 — 시뮬은 틱으로, 라이브는 텔레메트리나 사건 누적기가 채운다.
|
|
4
|
+
*
|
|
5
|
+
* 이름은 ISO 22400-2 의 시간 항목을 따른다.
|
|
6
|
+
*/
|
|
7
|
+
export interface OeeCounters {
|
|
8
|
+
/** 실 생산 시간(APT) — 실제로 만들고 있던 시간. 준비·대기·고장은 여기 들어가지 않는다. */
|
|
9
|
+
runMs: number;
|
|
10
|
+
/** 준비(AUST). */
|
|
11
|
+
setupMs: number;
|
|
12
|
+
/** 고장(ADOT). */
|
|
13
|
+
downMs: number;
|
|
14
|
+
goodCount: number;
|
|
15
|
+
scrapCount: number;
|
|
16
|
+
/** 계획정지 — 계획 조업 시간에서 뺀다(점심·예방보전·교대). */
|
|
17
|
+
holdMs?: number;
|
|
18
|
+
/** 언제부터 쟀나. 없으면 잰 구간이 없는 것이고, 그때는 가동률을 낼 수 없다. */
|
|
19
|
+
metricsSinceMs?: number;
|
|
20
|
+
/**
|
|
21
|
+
* **품목당 표준 시간**(PRI) — 성능률의 분자를 만든다.
|
|
22
|
+
*
|
|
23
|
+
* 없으면 성능률을 내지 않는다. 이 값이 없는데 실 가동 시간으로 대신 나누면 「얼마나 빨리
|
|
24
|
+
* 만들었나」가 아니라 「얼마나 쉬지 않았나」가 되고, 이름만 성능률이 된다.
|
|
25
|
+
*/
|
|
26
|
+
plannedRunTimePerItemMs?: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* **OEE** — ISO 22400-2 의 정의를 따른다.
|
|
30
|
+
*
|
|
31
|
+
* ```
|
|
32
|
+
* PBT 계획 조업 시간 = 잰 구간 − 계획정지
|
|
33
|
+
* APT 실 생산 시간 = runMs
|
|
34
|
+
* availability APT / PBT
|
|
35
|
+
* effectiveness PRI × PQ / APT PQ = 양품 + 불량
|
|
36
|
+
* quality GQ / PQ
|
|
37
|
+
* overall 셋의 곱
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* ── 예전 식과 무엇이 달랐나 (2026-08-30 대조) ────────────────────────────
|
|
41
|
+
* 예전에는 가동률의 분자가 `PBT − 준비 − 고장` 이었다. **대기가 분자에 남아 있었다.** 표준은
|
|
42
|
+
* `AOET = APT + AUST + ADET + ADOT` 로 대기도 실 생산 시간에서 뺀다.
|
|
43
|
+
*
|
|
44
|
+
* 총합은 같았다(가동률 × 성능률이 양쪽 다 `runMs / PBT` 로 떨어진다). 다른 것은 두 값을 따로 볼
|
|
45
|
+
* 때다. 자재를 기다린 시간이 성능률로 넘어가 있어서, **자재 문제를 설비 성능 문제로 읽게 했다.**
|
|
46
|
+
* 공장이 손 쓰는 방법이 그 둘에서 갈린다 — 가동률이 낮으면 보전·자재를 보고, 성능률이 낮으면
|
|
47
|
+
* 사이클 타임을 본다.
|
|
48
|
+
*
|
|
49
|
+
* 성능률도 달랐다. 표준은 품목당 표준 시간으로 재는데 우리는 실 가동 시간의 비율을 냈다.
|
|
50
|
+
*
|
|
51
|
+
* 대조는 원문이 아니라 그 표준을 구현한 규격에서 했다(OPC Foundation MachineTool §C.2). ISO 22400-2
|
|
52
|
+
* 원문은 유료라 읽지 못했다 — 이 주석이 근거의 한계를 함께 말한다.
|
|
53
|
+
*/
|
|
54
|
+
export declare function computeOee(c: OeeCounters, nowMs: number): OeeMetrics;
|
package/dist/oee.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **OEE** — ISO 22400-2 의 정의를 따른다.
|
|
3
|
+
*
|
|
4
|
+
* ```
|
|
5
|
+
* PBT 계획 조업 시간 = 잰 구간 − 계획정지
|
|
6
|
+
* APT 실 생산 시간 = runMs
|
|
7
|
+
* availability APT / PBT
|
|
8
|
+
* effectiveness PRI × PQ / APT PQ = 양품 + 불량
|
|
9
|
+
* quality GQ / PQ
|
|
10
|
+
* overall 셋의 곱
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* ── 예전 식과 무엇이 달랐나 (2026-08-30 대조) ────────────────────────────
|
|
14
|
+
* 예전에는 가동률의 분자가 `PBT − 준비 − 고장` 이었다. **대기가 분자에 남아 있었다.** 표준은
|
|
15
|
+
* `AOET = APT + AUST + ADET + ADOT` 로 대기도 실 생산 시간에서 뺀다.
|
|
16
|
+
*
|
|
17
|
+
* 총합은 같았다(가동률 × 성능률이 양쪽 다 `runMs / PBT` 로 떨어진다). 다른 것은 두 값을 따로 볼
|
|
18
|
+
* 때다. 자재를 기다린 시간이 성능률로 넘어가 있어서, **자재 문제를 설비 성능 문제로 읽게 했다.**
|
|
19
|
+
* 공장이 손 쓰는 방법이 그 둘에서 갈린다 — 가동률이 낮으면 보전·자재를 보고, 성능률이 낮으면
|
|
20
|
+
* 사이클 타임을 본다.
|
|
21
|
+
*
|
|
22
|
+
* 성능률도 달랐다. 표준은 품목당 표준 시간으로 재는데 우리는 실 가동 시간의 비율을 냈다.
|
|
23
|
+
*
|
|
24
|
+
* 대조는 원문이 아니라 그 표준을 구현한 규격에서 했다(OPC Foundation MachineTool §C.2). ISO 22400-2
|
|
25
|
+
* 원문은 유료라 읽지 못했다 — 이 주석이 근거의 한계를 함께 말한다.
|
|
26
|
+
*/
|
|
27
|
+
export function computeOee(c, nowMs) {
|
|
28
|
+
const missing = [];
|
|
29
|
+
/* 계획 조업 시간 — 잰 구간을 모르면 없다. 0 으로 메우면 「1970년부터 재고 있었다」가 된다. */
|
|
30
|
+
const planned = c.metricsSinceMs == null ? 0 : Math.max(0, nowMs - c.metricsSinceMs - (c.holdMs ?? 0));
|
|
31
|
+
if (planned <= 0)
|
|
32
|
+
missing.push('planned-busy-time');
|
|
33
|
+
/* 대기는 남는 시간이다 — 계획 조업에서 생산·준비·고장을 뺀 것. 따로 재지 않아도 나온다. */
|
|
34
|
+
const delayMs = Math.max(0, planned - c.runMs - c.setupMs - c.downMs);
|
|
35
|
+
const availability = planned > 0 ? Math.min(1, c.runMs / planned) : undefined;
|
|
36
|
+
const produced = c.goodCount + c.scrapCount;
|
|
37
|
+
if (produced <= 0)
|
|
38
|
+
missing.push('produced-quantity');
|
|
39
|
+
const quality = produced > 0 ? c.goodCount / produced : undefined;
|
|
40
|
+
/* 성능률 — 표준 시간이 없으면 내지 않는다. 실 생산 시간이 0 이어도 나눌 수 없다. */
|
|
41
|
+
if (c.plannedRunTimePerItemMs == null)
|
|
42
|
+
missing.push('planned-run-time-per-item');
|
|
43
|
+
else if (c.runMs <= 0)
|
|
44
|
+
missing.push('actual-production-time');
|
|
45
|
+
const performance = c.plannedRunTimePerItemMs != null && c.runMs > 0
|
|
46
|
+
? Math.min(1, (c.plannedRunTimePerItemMs * produced) / c.runMs)
|
|
47
|
+
: undefined;
|
|
48
|
+
const overall = availability != null && performance != null && quality != null ? availability * performance * quality : undefined;
|
|
49
|
+
return {
|
|
50
|
+
...(availability != null ? { availability } : {}),
|
|
51
|
+
...(performance != null ? { performance } : {}),
|
|
52
|
+
...(quality != null ? { quality } : {}),
|
|
53
|
+
...(overall != null ? { overall } : {}),
|
|
54
|
+
...(missing.length ? { missing } : {}),
|
|
55
|
+
runMs: c.runMs,
|
|
56
|
+
setupMs: c.setupMs,
|
|
57
|
+
downMs: c.downMs,
|
|
58
|
+
delayMs,
|
|
59
|
+
goodCount: c.goodCount,
|
|
60
|
+
scrapCount: c.scrapCount
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -60,6 +60,8 @@ const ASSET_STATUS = ['idle', 'in-use'];
|
|
|
60
60
|
const SPECS = {
|
|
61
61
|
task: {
|
|
62
62
|
eventType: OP_EVENT.task,
|
|
63
|
+
match: ['taskId'],
|
|
64
|
+
matchOrder: 50,
|
|
63
65
|
identity: 'taskId',
|
|
64
66
|
/* 종류가 없으면 성과를 종류별로 모을 수 없고(선언된 시간·수율이 종류로 붙는다) 지어낼 수도 없다. */
|
|
65
67
|
required: ['taskId', 'kind', 'status'],
|
|
@@ -79,6 +81,8 @@ const SPECS = {
|
|
|
79
81
|
},
|
|
80
82
|
equipment: {
|
|
81
83
|
eventType: OP_EVENT.equipment,
|
|
84
|
+
match: ['moverId'],
|
|
85
|
+
matchOrder: 20,
|
|
82
86
|
identity: 'moverId', // vocabulary-guard: allow 저널 와이어 필드(델타의 이름이 계약이다)
|
|
83
87
|
required: ['moverId', 'kind', 'status'], // vocabulary-guard: allow 위와 같은 이유
|
|
84
88
|
fields: {
|
|
@@ -94,6 +98,8 @@ const SPECS = {
|
|
|
94
98
|
},
|
|
95
99
|
person: {
|
|
96
100
|
eventType: OP_EVENT.person,
|
|
101
|
+
match: ['personId'],
|
|
102
|
+
matchOrder: 30,
|
|
97
103
|
identity: 'personId',
|
|
98
104
|
required: ['personId', 'status'],
|
|
99
105
|
fields: {
|
|
@@ -104,6 +110,8 @@ const SPECS = {
|
|
|
104
110
|
},
|
|
105
111
|
asset: {
|
|
106
112
|
eventType: OP_EVENT.asset,
|
|
113
|
+
match: ['assetId'],
|
|
114
|
+
matchOrder: 40,
|
|
107
115
|
identity: 'assetId',
|
|
108
116
|
required: ['assetId', 'status'],
|
|
109
117
|
fields: {
|
|
@@ -114,6 +122,8 @@ const SPECS = {
|
|
|
114
122
|
},
|
|
115
123
|
order: {
|
|
116
124
|
eventType: OP_EVENT.order,
|
|
125
|
+
match: ['orderId'],
|
|
126
|
+
matchOrder: 60,
|
|
117
127
|
identity: 'orderId',
|
|
118
128
|
/*
|
|
119
129
|
* 요청량·이행량을 **함께** 받는다. 없으면 리듀서가 진척을 0 으로 적는데(`requested ? … : 0`),
|
|
@@ -129,6 +139,8 @@ const SPECS = {
|
|
|
129
139
|
},
|
|
130
140
|
quality: {
|
|
131
141
|
eventType: OP_EVENT.quality,
|
|
142
|
+
match: ['moverId', 'good'],
|
|
143
|
+
matchOrder: 15,
|
|
132
144
|
identity: 'moverId', // vocabulary-guard: allow 저널 와이어 필드
|
|
133
145
|
/* 누적 카운터가 없으면 OEE 가 양품률을 못 센다 — 판정 하나만으로는 비율이 나오지 않는다. */
|
|
134
146
|
required: ['moverId', 'good', 'goodCount', 'scrapCount'], // vocabulary-guard: allow
|
|
@@ -170,6 +182,8 @@ const SPECS = {
|
|
|
170
182
|
*/
|
|
171
183
|
'equipment-period': {
|
|
172
184
|
eventType: OP_EVENT.equipmentPeriod,
|
|
185
|
+
match: ['moverId', 'status', 'from', 'to'],
|
|
186
|
+
matchOrder: 10,
|
|
173
187
|
identity: 'moverId', // vocabulary-guard: allow 저널 와이어 필드 — 전이와 같은 이름을 쓴다
|
|
174
188
|
required: ['moverId', 'status', 'from', 'to'], // vocabulary-guard: allow 위와 같은 이유
|
|
175
189
|
fields: {
|
|
@@ -196,6 +210,8 @@ const SPECS = {
|
|
|
196
210
|
*/
|
|
197
211
|
disposition: {
|
|
198
212
|
eventType: OP_EVENT.disposition,
|
|
213
|
+
match: ['subjectId', 'decision'],
|
|
214
|
+
matchOrder: 80,
|
|
199
215
|
identity: 'subjectId',
|
|
200
216
|
/* 무엇을 어떻게 하기로 했나 — 둘 중 하나가 없으면 그 결정은 아무 데도 붙지 못한다. */
|
|
201
217
|
required: ['subjectId', 'decision'],
|
|
@@ -207,6 +223,8 @@ const SPECS = {
|
|
|
207
223
|
},
|
|
208
224
|
test: {
|
|
209
225
|
eventType: OP_EVENT.test,
|
|
226
|
+
match: ['testableObjectId'],
|
|
227
|
+
matchOrder: 70,
|
|
210
228
|
identity: 'testableObjectId',
|
|
211
229
|
/* 무엇을 어느 기준으로 시험했나 — 둘 중 하나가 없으면 그 결과는 아무 데도 붙지 못한다. */
|
|
212
230
|
required: ['testableObjectId', 'specId'],
|
|
@@ -234,12 +252,16 @@ const SPECS = {
|
|
|
234
252
|
*/
|
|
235
253
|
complete: {
|
|
236
254
|
eventType: OP_EVENT.complete,
|
|
255
|
+
match: ['completeAxis'],
|
|
256
|
+
matchOrder: 100,
|
|
237
257
|
identity: 'completeAxis',
|
|
238
258
|
required: ['completeAxis', 'since'],
|
|
239
259
|
fields: { completeAxis: 'string', since: 'string', recordTime: 'string' }
|
|
240
260
|
},
|
|
241
261
|
observation: {
|
|
242
262
|
eventType: OP_EVENT.observation,
|
|
263
|
+
match: ['locationId', 'propertyId'],
|
|
264
|
+
matchOrder: 90,
|
|
243
265
|
identity: 'locationId',
|
|
244
266
|
/* 자리와 속성 — 둘 중 하나가 없으면 그 관측은 아무 데도 붙지 못한다. */
|
|
245
267
|
required: ['locationId', 'propertyId'],
|
|
@@ -268,68 +290,34 @@ export function operationalKindOf(record) {
|
|
|
268
290
|
if (!record || typeof record !== 'object')
|
|
269
291
|
return undefined;
|
|
270
292
|
const r = record;
|
|
271
|
-
if (r.epc !== undefined || r.meterId !== undefined || r.equipmentId !== undefined)
|
|
272
|
-
return undefined;
|
|
273
|
-
const has = (k) => typeof r[k] === 'string' && r[k].trim().length > 0;
|
|
274
|
-
/* vocabulary-guard: allow 저널 와이어 필드로 가른다 */
|
|
275
|
-
/*
|
|
276
|
-
* 구간이 있으면 **마감된 상태 구간**이지 시점의 전이가 아니다. 전이보다 먼저 본다 — 뒤에 두면
|
|
277
|
-
* 사람이 되돌아가 적은 구간이 「지금 이 상태다」로 읽혀 트윈의 상태가 과거로 끌린다.
|
|
278
|
-
*/
|
|
279
|
-
if (has('moverId') && has('from') && has('to'))
|
|
280
|
-
return 'equipment-period';
|
|
281
|
-
if (has('moverId'))
|
|
282
|
-
return r.good !== undefined ? 'quality' : 'equipment';
|
|
283
|
-
if (has('personId'))
|
|
284
|
-
return 'person';
|
|
285
|
-
if (has('assetId'))
|
|
286
|
-
return 'asset';
|
|
287
|
-
if (has('taskId'))
|
|
288
|
-
return 'task'; // 작업이 든 `orderId` 는 소속(참조)이다
|
|
289
|
-
if (has('orderId'))
|
|
290
|
-
return 'order';
|
|
291
|
-
/*
|
|
292
|
-
* ── ★ **채널을 열고 들어오는 길을 내지 않았다** (2026-08-24) ─────────────────
|
|
293
|
-
* `OP_EVENT.test` 를 계약에 냈는데 이 라우팅이 `testableObjectId` 를 보지 않았다. 그래서 커넥터의
|
|
294
|
-
* 시험 결과가 **어느 통도 아니어서 조용히 버려졌다** — 거부 목록에도 남지 않았다(운영 경로를 아예
|
|
295
|
-
* 지나지 않으므로).
|
|
296
|
-
*
|
|
297
|
-
* 같은 부류가 하루에 세 번 났다: `ilmd`(매핑에 자리 없음) · 사건 시각(이름 어긋남) · 그리고 이것.
|
|
298
|
-
* **계약에 자리를 만드는 것과 그 자리로 가는 길을 내는 것은 다른 일이다.** 앞의 것만 하면 보내는
|
|
299
|
-
* 쪽에는 「실었다」로 보이고 화면에는 「없다」로 보인다.
|
|
300
|
-
*
|
|
301
|
-
* 순서상 뒤에 둔다 — 시험 결과가 작업·오더를 함께 가리킬 수 있고, 그때 그것은 **그 작업의 사실**이다.
|
|
302
|
-
*/
|
|
303
|
-
if (has('testableObjectId'))
|
|
304
|
-
return 'test';
|
|
305
|
-
/*
|
|
306
|
-
* **처분** — 같은 부류가 또 났다(2026-08-30). `OP_EVENT.disposition` 을 계약에 내고 여기로 오는 길을
|
|
307
|
-
* 내지 않아, MES 가 보내면 어느 통도 아니어서 사라졌다. 위 주석이 적은 것이 **셋째**다.
|
|
308
|
-
*
|
|
309
|
-
* 시험 결과 뒤에 둔다 — 처분은 판정을 가리킬 수 있고(`specId`), 그때 정체성은 처분 대상이지
|
|
310
|
-
* 시험 대상이 아니다. 앞에 두면 판정에 딸린 처분이 시험으로 읽힌다.
|
|
311
|
-
*/
|
|
312
|
-
if (has('subjectId') && has('decision'))
|
|
313
|
-
return 'disposition';
|
|
314
293
|
/*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
* (`observationAt`)까지 붙였는데 **이 라우팅이 `locationId` 를 보지 않았다.** 커넥터가 방의 온습도를
|
|
318
|
-
* 실어 보내면 「어느 운영 사실인지 모른다」로 거부됐다.
|
|
319
|
-
*
|
|
320
|
-
* 같은 부류를 하루에 일곱 번 만났다. 다만 이번엔 **거부되고 이유가 남았다** — 시험 결과 때는 어느
|
|
321
|
-
* 통도 아니어서 조용히 사라졌다. 그 차이가 이것을 5분 만에 찾게 했다(§`isOperationalRecord`).
|
|
322
|
-
*
|
|
323
|
-
* **둘을 함께 요구한다.** `locationId` 만으로는 자리를 말하는 다른 사실과 섞인다. 관측은 「어느
|
|
324
|
-
* 자리의 **무엇**을 쟀나」이므로 속성 없이는 담을 곳이 없다 — 그때는 받지 않는 것이 옳다.
|
|
294
|
+
* EPCIS·에너지와 겹치지 않게 본다: `epc`·`meterId` 가 있으면 그쪽 어휘이고, `equipmentId` 는 설비
|
|
295
|
+
* **에너지** 상태의 이름이다(운영 설비는 `moverId`).
|
|
325
296
|
*/
|
|
326
|
-
if (
|
|
327
|
-
return
|
|
328
|
-
/*
|
|
329
|
-
|
|
330
|
-
|
|
297
|
+
if (r.epc !== undefined || r.meterId !== undefined || r.equipmentId !== undefined)
|
|
298
|
+
return undefined;
|
|
299
|
+
/* 문자열은 비어 있으면 없는 것으로 본다 — 공백만 실은 필드는 말한 것이 아니다. */
|
|
300
|
+
const present = (k) => {
|
|
301
|
+
const v = r[k];
|
|
302
|
+
if (v === undefined || v === null)
|
|
303
|
+
return false;
|
|
304
|
+
return typeof v === 'string' ? v.trim().length > 0 : true;
|
|
305
|
+
};
|
|
306
|
+
for (const kind of MATCH_ORDER) {
|
|
307
|
+
const spec = SPECS[kind];
|
|
308
|
+
const fields = spec.match ?? [spec.identity];
|
|
309
|
+
if (fields.every(present))
|
|
310
|
+
return kind;
|
|
311
|
+
}
|
|
331
312
|
return undefined;
|
|
332
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* 판정 순서 — 표의 `matchOrder` 에서 한 번만 만든다.
|
|
316
|
+
*
|
|
317
|
+
* 좁은 것이 먼저다. 그리고 자원(설비·사람·자산)을 작업보다, 작업을 오더보다 먼저 본다 — 정체 필드는
|
|
318
|
+
* 하나만 오지 않고, 아무거나 먼저 보면 **참조를 주체로 읽는다**(설비 델타가 든 `taskId` 는 소속이다).
|
|
319
|
+
*/
|
|
320
|
+
const MATCH_ORDER = Object.keys(SPECS).sort((a, b) => (SPECS[a].matchOrder ?? 1000) - (SPECS[b].matchOrder ?? 1000));
|
|
333
321
|
/** 이 레코드가 운영 사실인가 — 호스트의 라우팅이 묻는 자리. */
|
|
334
322
|
export function isOperationalRecord(record) {
|
|
335
323
|
return operationalKindOf(record) !== undefined;
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CONTRACT_VERSION = "0.
|
|
1
|
+
export declare const CONTRACT_VERSION = "0.5.0";
|
package/dist/version.js
CHANGED
package/dist-cjs/index.cjs
CHANGED
|
@@ -80,6 +80,7 @@ __export(index_exports, {
|
|
|
80
80
|
classClosure: () => classClosure,
|
|
81
81
|
classIdentifierViolation: () => classIdentifierViolation,
|
|
82
82
|
commandsOf: () => commandsOf,
|
|
83
|
+
computeOee: () => computeOee,
|
|
83
84
|
conversionFactorOf: () => conversionFactorOf,
|
|
84
85
|
criterionSaysNothing: () => criterionSaysNothing,
|
|
85
86
|
documentPath: () => documentPath,
|
|
@@ -3164,6 +3165,8 @@ var ASSET_STATUS = ["idle", "in-use"];
|
|
|
3164
3165
|
var SPECS = {
|
|
3165
3166
|
task: {
|
|
3166
3167
|
eventType: OP_EVENT.task,
|
|
3168
|
+
match: ["taskId"],
|
|
3169
|
+
matchOrder: 50,
|
|
3167
3170
|
identity: "taskId",
|
|
3168
3171
|
/* 종류가 없으면 성과를 종류별로 모을 수 없고(선언된 시간·수율이 종류로 붙는다) 지어낼 수도 없다. */
|
|
3169
3172
|
required: ["taskId", "kind", "status"],
|
|
@@ -3200,6 +3203,8 @@ var SPECS = {
|
|
|
3200
3203
|
},
|
|
3201
3204
|
equipment: {
|
|
3202
3205
|
eventType: OP_EVENT.equipment,
|
|
3206
|
+
match: ["moverId"],
|
|
3207
|
+
matchOrder: 20,
|
|
3203
3208
|
identity: "moverId",
|
|
3204
3209
|
// vocabulary-guard: allow 저널 와이어 필드(델타의 이름이 계약이다)
|
|
3205
3210
|
required: ["moverId", "kind", "status"],
|
|
@@ -3226,6 +3231,8 @@ var SPECS = {
|
|
|
3226
3231
|
},
|
|
3227
3232
|
person: {
|
|
3228
3233
|
eventType: OP_EVENT.person,
|
|
3234
|
+
match: ["personId"],
|
|
3235
|
+
matchOrder: 30,
|
|
3229
3236
|
identity: "personId",
|
|
3230
3237
|
required: ["personId", "status"],
|
|
3231
3238
|
fields: {
|
|
@@ -3243,6 +3250,8 @@ var SPECS = {
|
|
|
3243
3250
|
},
|
|
3244
3251
|
asset: {
|
|
3245
3252
|
eventType: OP_EVENT.asset,
|
|
3253
|
+
match: ["assetId"],
|
|
3254
|
+
matchOrder: 40,
|
|
3246
3255
|
identity: "assetId",
|
|
3247
3256
|
required: ["assetId", "status"],
|
|
3248
3257
|
fields: {
|
|
@@ -3260,6 +3269,8 @@ var SPECS = {
|
|
|
3260
3269
|
},
|
|
3261
3270
|
order: {
|
|
3262
3271
|
eventType: OP_EVENT.order,
|
|
3272
|
+
match: ["orderId"],
|
|
3273
|
+
matchOrder: 60,
|
|
3263
3274
|
identity: "orderId",
|
|
3264
3275
|
/*
|
|
3265
3276
|
* 요청량·이행량을 **함께** 받는다. 없으면 리듀서가 진척을 0 으로 적는데(`requested ? … : 0`),
|
|
@@ -3289,6 +3300,8 @@ var SPECS = {
|
|
|
3289
3300
|
},
|
|
3290
3301
|
quality: {
|
|
3291
3302
|
eventType: OP_EVENT.quality,
|
|
3303
|
+
match: ["moverId", "good"],
|
|
3304
|
+
matchOrder: 15,
|
|
3292
3305
|
identity: "moverId",
|
|
3293
3306
|
// vocabulary-guard: allow 저널 와이어 필드
|
|
3294
3307
|
/* 누적 카운터가 없으면 OEE 가 양품률을 못 센다 — 판정 하나만으로는 비율이 나오지 않는다. */
|
|
@@ -3333,6 +3346,8 @@ var SPECS = {
|
|
|
3333
3346
|
*/
|
|
3334
3347
|
"equipment-period": {
|
|
3335
3348
|
eventType: OP_EVENT.equipmentPeriod,
|
|
3349
|
+
match: ["moverId", "status", "from", "to"],
|
|
3350
|
+
matchOrder: 10,
|
|
3336
3351
|
identity: "moverId",
|
|
3337
3352
|
// vocabulary-guard: allow 저널 와이어 필드 — 전이와 같은 이름을 쓴다
|
|
3338
3353
|
required: ["moverId", "status", "from", "to"],
|
|
@@ -3367,6 +3382,8 @@ var SPECS = {
|
|
|
3367
3382
|
*/
|
|
3368
3383
|
disposition: {
|
|
3369
3384
|
eventType: OP_EVENT.disposition,
|
|
3385
|
+
match: ["subjectId", "decision"],
|
|
3386
|
+
matchOrder: 80,
|
|
3370
3387
|
identity: "subjectId",
|
|
3371
3388
|
/* 무엇을 어떻게 하기로 했나 — 둘 중 하나가 없으면 그 결정은 아무 데도 붙지 못한다. */
|
|
3372
3389
|
required: ["subjectId", "decision"],
|
|
@@ -3385,6 +3402,8 @@ var SPECS = {
|
|
|
3385
3402
|
},
|
|
3386
3403
|
test: {
|
|
3387
3404
|
eventType: OP_EVENT.test,
|
|
3405
|
+
match: ["testableObjectId"],
|
|
3406
|
+
matchOrder: 70,
|
|
3388
3407
|
identity: "testableObjectId",
|
|
3389
3408
|
/* 무엇을 어느 기준으로 시험했나 — 둘 중 하나가 없으면 그 결과는 아무 데도 붙지 못한다. */
|
|
3390
3409
|
required: ["testableObjectId", "specId"],
|
|
@@ -3418,12 +3437,16 @@ var SPECS = {
|
|
|
3418
3437
|
*/
|
|
3419
3438
|
complete: {
|
|
3420
3439
|
eventType: OP_EVENT.complete,
|
|
3440
|
+
match: ["completeAxis"],
|
|
3441
|
+
matchOrder: 100,
|
|
3421
3442
|
identity: "completeAxis",
|
|
3422
3443
|
required: ["completeAxis", "since"],
|
|
3423
3444
|
fields: { completeAxis: "string", since: "string", recordTime: "string" }
|
|
3424
3445
|
},
|
|
3425
3446
|
observation: {
|
|
3426
3447
|
eventType: OP_EVENT.observation,
|
|
3448
|
+
match: ["locationId", "propertyId"],
|
|
3449
|
+
matchOrder: 90,
|
|
3427
3450
|
identity: "locationId",
|
|
3428
3451
|
/* 자리와 속성 — 둘 중 하나가 없으면 그 관측은 아무 데도 붙지 못한다. */
|
|
3429
3452
|
required: ["locationId", "propertyId"],
|
|
@@ -3445,19 +3468,21 @@ function operationalKindOf(record) {
|
|
|
3445
3468
|
if (!record || typeof record !== "object") return void 0;
|
|
3446
3469
|
const r = record;
|
|
3447
3470
|
if (r.epc !== void 0 || r.meterId !== void 0 || r.equipmentId !== void 0) return void 0;
|
|
3448
|
-
const
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
if (has("completeAxis")) return "complete";
|
|
3471
|
+
const present = (k) => {
|
|
3472
|
+
const v = r[k];
|
|
3473
|
+
if (v === void 0 || v === null) return false;
|
|
3474
|
+
return typeof v === "string" ? v.trim().length > 0 : true;
|
|
3475
|
+
};
|
|
3476
|
+
for (const kind of MATCH_ORDER) {
|
|
3477
|
+
const spec = SPECS[kind];
|
|
3478
|
+
const fields = spec.match ?? [spec.identity];
|
|
3479
|
+
if (fields.every(present)) return kind;
|
|
3480
|
+
}
|
|
3459
3481
|
return void 0;
|
|
3460
3482
|
}
|
|
3483
|
+
var MATCH_ORDER = Object.keys(SPECS).sort(
|
|
3484
|
+
(a, b) => (SPECS[a].matchOrder ?? 1e3) - (SPECS[b].matchOrder ?? 1e3)
|
|
3485
|
+
);
|
|
3461
3486
|
function isOperationalRecord(record) {
|
|
3462
3487
|
return operationalKindOf(record) !== void 0;
|
|
3463
3488
|
}
|
|
@@ -3733,7 +3758,36 @@ function retiredVocabularyIn(line) {
|
|
|
3733
3758
|
}
|
|
3734
3759
|
|
|
3735
3760
|
// src/version.ts
|
|
3736
|
-
var CONTRACT_VERSION = "0.
|
|
3761
|
+
var CONTRACT_VERSION = "0.5.0";
|
|
3762
|
+
|
|
3763
|
+
// src/oee.ts
|
|
3764
|
+
function computeOee(c, nowMs) {
|
|
3765
|
+
const missing = [];
|
|
3766
|
+
const planned = c.metricsSinceMs == null ? 0 : Math.max(0, nowMs - c.metricsSinceMs - (c.holdMs ?? 0));
|
|
3767
|
+
if (planned <= 0) missing.push("planned-busy-time");
|
|
3768
|
+
const delayMs = Math.max(0, planned - c.runMs - c.setupMs - c.downMs);
|
|
3769
|
+
const availability = planned > 0 ? Math.min(1, c.runMs / planned) : void 0;
|
|
3770
|
+
const produced = c.goodCount + c.scrapCount;
|
|
3771
|
+
if (produced <= 0) missing.push("produced-quantity");
|
|
3772
|
+
const quality = produced > 0 ? c.goodCount / produced : void 0;
|
|
3773
|
+
if (c.plannedRunTimePerItemMs == null) missing.push("planned-run-time-per-item");
|
|
3774
|
+
else if (c.runMs <= 0) missing.push("actual-production-time");
|
|
3775
|
+
const performance = c.plannedRunTimePerItemMs != null && c.runMs > 0 ? Math.min(1, c.plannedRunTimePerItemMs * produced / c.runMs) : void 0;
|
|
3776
|
+
const overall = availability != null && performance != null && quality != null ? availability * performance * quality : void 0;
|
|
3777
|
+
return {
|
|
3778
|
+
...availability != null ? { availability } : {},
|
|
3779
|
+
...performance != null ? { performance } : {},
|
|
3780
|
+
...quality != null ? { quality } : {},
|
|
3781
|
+
...overall != null ? { overall } : {},
|
|
3782
|
+
...missing.length ? { missing } : {},
|
|
3783
|
+
runMs: c.runMs,
|
|
3784
|
+
setupMs: c.setupMs,
|
|
3785
|
+
downMs: c.downMs,
|
|
3786
|
+
delayMs,
|
|
3787
|
+
goodCount: c.goodCount,
|
|
3788
|
+
scrapCount: c.scrapCount
|
|
3789
|
+
};
|
|
3790
|
+
}
|
|
3737
3791
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3738
3792
|
0 && (module.exports = {
|
|
3739
3793
|
BIZSTEP,
|
|
@@ -3797,6 +3851,7 @@ var CONTRACT_VERSION = "0.4.0";
|
|
|
3797
3851
|
classClosure,
|
|
3798
3852
|
classIdentifierViolation,
|
|
3799
3853
|
commandsOf,
|
|
3854
|
+
computeOee,
|
|
3800
3855
|
conversionFactorOf,
|
|
3801
3856
|
criterionSaysNothing,
|
|
3802
3857
|
documentPath,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@operato/ops-contract",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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",
|