@operato/ops-contract 0.2.0 → 0.4.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 +12 -3
- package/dist/contract.js +9 -0
- package/dist/domain-definition.d.ts +2 -1
- package/dist/domain-definition.js +2 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/operational-ingest.d.ts +1 -1
- package/dist/operational-ingest.js +109 -3
- package/dist/version.d.ts +1 -0
- package/dist/version.js +14 -0
- package/dist-cjs/index.cjs +103 -2
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -555,7 +555,7 @@ export interface TestSpecificationCriterion {
|
|
|
555
555
|
/** 표준 `EvaluatedPropertyID` — 이 기준이 **무엇을 재어** 판정하나. 측정값과 짝을 맞추는 키다. */
|
|
556
556
|
evaluatedPropertyId?: string;
|
|
557
557
|
}
|
|
558
|
-
export interface TestSpecification {
|
|
558
|
+
export interface TestSpecification extends EffectivePeriod {
|
|
559
559
|
/** 표준 `ID` — 자원의 `testSpecificationIds` 가 이 값을 가리킨다. */
|
|
560
560
|
id: string;
|
|
561
561
|
/** 표준 `Description`. i18n 키일 수 있다(사람 언어는 표현 계층이 렌더한다). */
|
|
@@ -2513,6 +2513,15 @@ export declare const OP_EVENT: {
|
|
|
2513
2513
|
* 이 채널이 없으면 시험 결과는 **상태에만 있는 축**이 된다 — 재기동에서 사라지고, 폴드가 되살릴 수
|
|
2514
2514
|
* 없고, 미러가 이어받지 못한다(§상태 ⊆ 이벤트).
|
|
2515
2515
|
*/
|
|
2516
|
+
/**
|
|
2517
|
+
* **마감된 설비 상태 구간** — 이 설비가 이 구간 동안 이 상태였다.
|
|
2518
|
+
*
|
|
2519
|
+
* 시점의 전이(`equipment.status`)와 다른 사실이다. 전이는 설비가 내고, 이것은 **사람이 나중에
|
|
2520
|
+
* 적는다.** 자재 대기처럼 신호를 내지 않는 정지는 이 길로만 들어온다.
|
|
2521
|
+
*
|
|
2522
|
+
* ISO 22400-2 의 가동률 계산이 이 구간들을 읽는다.
|
|
2523
|
+
*/
|
|
2524
|
+
readonly equipmentPeriod: "equipment.state.period";
|
|
2516
2525
|
readonly test: "test.result";
|
|
2517
2526
|
/**
|
|
2518
2527
|
* **부적합 처분** — 재고 판정을 받은 것을 어떻게 하기로 정했나(재작업 · 특채 · 폐기 · 반품).
|
|
@@ -3219,7 +3228,7 @@ export interface TwinModelDef {
|
|
|
3219
3228
|
*/
|
|
3220
3229
|
identity?: IdentityDeclaration;
|
|
3221
3230
|
/** parallelism = 동시 처리 수(LocationState.parallelism 참조). capacity 는 저장 용량. */
|
|
3222
|
-
locations: {
|
|
3231
|
+
locations: (EffectivePeriod & {
|
|
3223
3232
|
id: string;
|
|
3224
3233
|
type: string;
|
|
3225
3234
|
capacity: number;
|
|
@@ -3227,7 +3236,7 @@ export interface TwinModelDef {
|
|
|
3227
3236
|
parentId?: string;
|
|
3228
3237
|
properties?: ResourceProperty[];
|
|
3229
3238
|
testSpecificationIds?: TestSpecificationRefs;
|
|
3230
|
-
}[];
|
|
3239
|
+
})[];
|
|
3231
3240
|
/**
|
|
3232
3241
|
* 설비(설비). mtbfMs/mttrMs 지정 시 확률적 고장 모델 참여(OEE Availability 손실). 미지정=고장 없음.
|
|
3233
3242
|
* `window` 지정 시 그 시간대에만 일한다(교대·가동시간) — 미지정이면 24시간 가용(기존 거동).
|
package/dist/contract.js
CHANGED
|
@@ -986,6 +986,15 @@ export const OP_EVENT = {
|
|
|
986
986
|
* 이 채널이 없으면 시험 결과는 **상태에만 있는 축**이 된다 — 재기동에서 사라지고, 폴드가 되살릴 수
|
|
987
987
|
* 없고, 미러가 이어받지 못한다(§상태 ⊆ 이벤트).
|
|
988
988
|
*/
|
|
989
|
+
/**
|
|
990
|
+
* **마감된 설비 상태 구간** — 이 설비가 이 구간 동안 이 상태였다.
|
|
991
|
+
*
|
|
992
|
+
* 시점의 전이(`equipment.status`)와 다른 사실이다. 전이는 설비가 내고, 이것은 **사람이 나중에
|
|
993
|
+
* 적는다.** 자재 대기처럼 신호를 내지 않는 정지는 이 길로만 들어온다.
|
|
994
|
+
*
|
|
995
|
+
* ISO 22400-2 의 가동률 계산이 이 구간들을 읽는다.
|
|
996
|
+
*/
|
|
997
|
+
equipmentPeriod: 'equipment.state.period',
|
|
989
998
|
test: 'test.result',
|
|
990
999
|
/**
|
|
991
1000
|
* **부적합 처분** — 재고 판정을 받은 것을 어떻게 하기로 정했나(재작업 · 특채 · 폐기 · 반품).
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { EffectivePeriod } from './contract.ts';
|
|
1
2
|
/** 표준 온톨로지 투영(열린 문자열 — 하드코딩 enum 금지). */
|
|
2
3
|
/**
|
|
3
4
|
* 표준 온톨로지 투영 — 이 개념을 각 표준이 무엇이라 부르나.
|
|
@@ -129,7 +130,7 @@ export interface DurationVariability {
|
|
|
129
130
|
* 걸리나·얼마나 성공하나" 는 커널 소스의 상수(30·20·40초, 수율 0.8)였다. 그래서 현장마다 다른 값을
|
|
130
131
|
* 데이터로 줄 방법이 없었고, 그 상수가 주목 신호(불량률 높음)까지 만들어 냈다.
|
|
131
132
|
*/
|
|
132
|
-
export interface OperationDef {
|
|
133
|
+
export interface OperationDef extends EffectivePeriod {
|
|
133
134
|
key: string;
|
|
134
135
|
label: string;
|
|
135
136
|
intent: OperationIntent;
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* 커널이 "무엇이 있고 어떻게 흐르나"(타입 + 공정 route/BOM)를 **데이터로** 받는 형식. zero-dep(자기 타입).
|
|
4
4
|
*
|
|
5
5
|
* 특정 공정이 커널 코드에 하드코딩되던 것을 이 데이터 계약으로 대체한다(design/plans/domain-catalog-layering.md).
|
|
6
|
-
* 스토어 패키지 `@operato/twin-catalog` 가 이 타입을 import
|
|
6
|
+
* 스토어 패키지 `@operato/twin-catalog` 가 이 타입을 import type { EffectivePeriod } from './contract.ts'
|
|
7
|
+
import 해 스토어 메타(version/supplier)를 얹어 배포한다(catalog → kernel 의존).
|
|
7
8
|
* 표준 앵커: GS1 EPCIS 2.0(bizStep) · ISA-95(WorkCenter·OperationsDefinition·BOM) · ISO 55000(Asset).
|
|
8
9
|
*/
|
|
9
10
|
/**
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { IngestResult } from './face2-adapter.ts';
|
|
2
2
|
/** 이 문이 받는 여섯 가지 — 리듀서가 다루는 것과 같은 목록(주목 확인은 우리 안의 행위라 제외). */
|
|
3
|
-
export type OperationalKind = 'task' | 'equipment' | 'person' | 'asset' | 'order' | 'quality' | 'test' | 'observation' | 'complete';
|
|
3
|
+
export type OperationalKind = 'task' | 'equipment' | 'equipment-period' | 'person' | 'asset' | 'order' | 'quality' | 'test' | 'disposition' | 'observation' | 'complete';
|
|
4
4
|
/**
|
|
5
5
|
* 정규 운영 레코드 — **델타의 필드 이름 + 시각(`at`)**.
|
|
6
6
|
*
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* 모르는 필드는 **조용히 버리지 않고 거부한다.** `taskID` 처럼 한 글자 틀린 이름은 통과시키면 영원히
|
|
35
35
|
* 보이지 않는 손실이 된다(이 문에는 아직 옛 발신자가 없어 호환 부담도 없다).
|
|
36
36
|
*/
|
|
37
|
-
import { OP_EVENT } from "./contract.js";
|
|
37
|
+
import { OP_EVENT, DISPOSITION_DECISION } from "./contract.js";
|
|
38
38
|
/**
|
|
39
39
|
* 닫아 둔 낱말과 그 이유.
|
|
40
40
|
* · 작업 상태 — 성과 폴드가 `completed`·`in-progress` 로 갈린다(`kpi-fold`).
|
|
@@ -42,7 +42,19 @@ import { OP_EVENT } from "./contract.js";
|
|
|
42
42
|
* · 사람·자산 상태 — 배정이 `idle` 을 찾는다. 다른 낱말이면 있는 자원이 없는 것이 된다.
|
|
43
43
|
*/
|
|
44
44
|
const TASK_STATUS = ['created', 'assigned', 'in-progress', 'completed'];
|
|
45
|
-
|
|
45
|
+
/*
|
|
46
|
+
* 설비 상태 — **ISO 22400-2 의 시간 모델에 하나씩 대응한다**(2026-08-30).
|
|
47
|
+
*
|
|
48
|
+
* busy actual production time 가동률·성능률의 분자
|
|
49
|
+
* setup actual unit setup time 준비. 가동에서 빼되 계획 조업에는 남는다
|
|
50
|
+
* down actual unit down time 고장. 가동률을 깎는다
|
|
51
|
+
* idle actual unit delay time 대기(자재·앞공정·인원). 가동률을 깎는다
|
|
52
|
+
* planned-stop 계획 조업 시간에서 제외 점심·예방보전·교대. 깎지 않는다
|
|
53
|
+
*
|
|
54
|
+
* `down` 과 `idle` 은 22400 에서 다른 항목이고 공장이 손 쓰는 방법도 다르다(고장은 보전, 대기는
|
|
55
|
+
* 자재·일정). 처음부터 갈라 두었던 것이 맞았고, 여기에 둘을 더해 22400 의 시간 모델을 덮는다.
|
|
56
|
+
*/
|
|
57
|
+
const EQUIPMENT_STATUS = ['idle', 'busy', 'down', 'setup', 'planned-stop'];
|
|
46
58
|
const PERSON_STATUS = ['idle', 'busy'];
|
|
47
59
|
const ASSET_STATUS = ['idle', 'in-use'];
|
|
48
60
|
const SPECS = {
|
|
@@ -72,6 +84,8 @@ const SPECS = {
|
|
|
72
84
|
fields: {
|
|
73
85
|
moverId: 'string', kind: 'string', status: 'string', location: 'string', homeLocation: 'string', // vocabulary-guard: allow
|
|
74
86
|
taskId: 'string', held: 'boolean', effectiveStart: 'string', effectiveEnd: 'string', recordTime: 'string',
|
|
87
|
+
/* 왜 이 상태가 됐나 — 설비가 알려 주면 싣는다. 사람이 나중에 정하는 사유는 구간 사실에 적는다. */
|
|
88
|
+
reasonCode: 'string',
|
|
75
89
|
/* 이동 구간 — 실 시스템도 줄 수 있는 사실이다(AGV·RTLS 가 출발·도착·소요를 낸다). 안쪽 필드까지
|
|
76
90
|
재검사하지는 않는다: 그 모양은 `EquipmentMotion` 계약이고, 여기서 두 번 지키면 두 벌이 된다. */
|
|
77
91
|
motion: 'object'
|
|
@@ -130,6 +144,67 @@ const SPECS = {
|
|
|
130
144
|
* `propertyMeasurements` 안쪽은 재검사하지 않는다 — 그 모양은 `PropertyMeasurement` 계약이고,
|
|
131
145
|
* 여기서 두 번 지키면 두 벌이 된다(설비 `motion` 과 같은 규율).
|
|
132
146
|
*/
|
|
147
|
+
/*
|
|
148
|
+
* **마감된 설비 상태 구간** — 이 설비가 이 구간 동안 이 상태였다.
|
|
149
|
+
*
|
|
150
|
+
* ── 전이만으로는 안 되는 이유 (2026-08-30) ───────────────────────────────
|
|
151
|
+
* 세 가지다. 셋째가 결정적이다.
|
|
152
|
+
*
|
|
153
|
+
* 사유가 붙을 자리가 없다 기계가 서는 순간에는 왜 섰는지 아무도 모른다. 작업자가 라인이 다시
|
|
154
|
+
* 돈 뒤에 적는다. 그 사이에 전이가 더 있으면 어느 전이에 붙일지 정할 수 없다
|
|
155
|
+
* 계획·비계획을 나중에 정한다 ISO 22400 은 계획정지를 계획 조업 시간에서 빼고 고장은 빼지 않는다.
|
|
156
|
+
* 그 분류는 전이가 일어난 순간에 모른다
|
|
157
|
+
* 전이를 내지 않는 정지가 많다 자재 대기 · 앞 공정 대기 · 작업자 부재. 기계는 전원이 켜진 채 `idle`
|
|
158
|
+
* 이고 신호가 하나도 안 나온다. OEE 에서 가장 크게 깎이는 것이 보통 이 시간이다
|
|
159
|
+
*
|
|
160
|
+
* 마지막이 감시 시스템과 MES 가 갈리는 자리이기도 하다. 설비가 말하는 것만 모으면 감시이고,
|
|
161
|
+
* **설비가 말하지 못하는 것을 사람이 적어 넣는 자리**가 MES 다.
|
|
162
|
+
*
|
|
163
|
+
* ── 이름이 `downtime` 이 아닌 이유 ────────────────────────────────────────
|
|
164
|
+
* 이 구간은 `setup` 과 `planned-stop` 도 나른다. 그 둘은 정지가 아니다. 담는 것은 **상태 구간**이고,
|
|
165
|
+
* 어느 상태인지는 `status` 가 말한다.
|
|
166
|
+
*
|
|
167
|
+
* ── 되돌아가 적는 사실이다 ───────────────────────────────────────────────
|
|
168
|
+
* 봉투의 사건 시각은 **구간의 끝**(그때 성립한다)이고, 적은 시각은 `recordTime` 이다. 둘이 갈려 있어야
|
|
169
|
+
* 「언제 일어났나」와 「언제 알았나」를 구별할 수 있다.
|
|
170
|
+
*/
|
|
171
|
+
'equipment-period': {
|
|
172
|
+
eventType: OP_EVENT.equipmentPeriod,
|
|
173
|
+
identity: 'moverId', // vocabulary-guard: allow 저널 와이어 필드 — 전이와 같은 이름을 쓴다
|
|
174
|
+
required: ['moverId', 'status', 'from', 'to'], // vocabulary-guard: allow 위와 같은 이유
|
|
175
|
+
fields: {
|
|
176
|
+
moverId: 'string', status: 'string', from: 'string', to: 'string', // vocabulary-guard: allow
|
|
177
|
+
/* 사람이 정한 사유 — 없을 수 있다(적지 않은 것과 사유가 없는 것은 다르므로 지어내지 않는다). */
|
|
178
|
+
reasonCode: 'string', decidedBy: 'string', recordTime: 'string'
|
|
179
|
+
},
|
|
180
|
+
enums: { status: EQUIPMENT_STATUS }
|
|
181
|
+
},
|
|
182
|
+
/*
|
|
183
|
+
* **부적합 처분** — 재고 판정을 받은 것을 어떻게 하기로 정했나.
|
|
184
|
+
*
|
|
185
|
+
* ── 이 통이 늦게 생긴 이유 (2026-08-30) ──────────────────────────────────
|
|
186
|
+
* 사건 이름(`OP_EVENT.disposition`)과 모양(`DispositionFact`)을 먼저 만들고 **여기로 오는 길을
|
|
187
|
+
* 내지 않았다.** 그래서 처분 사실을 보내면 어느 통도 아니어서 떨어진 목록에도 남지 않고 사라졌다.
|
|
188
|
+
* MES 전문가가 소스를 읽다 찾았다.
|
|
189
|
+
*
|
|
190
|
+
* 이 파일의 위 주석이 말한 부류 그대로다 — `OP_EVENT.test` 와 `OP_EVENT.observation` 이 같은 일을
|
|
191
|
+
* 겪었고 이번이 셋째다. **계약에 자리를 만드는 것과 그 자리로 가는 길을 내는 것은 다른 일이다.**
|
|
192
|
+
*
|
|
193
|
+
* ── 판정과 다른 사실이다 ─────────────────────────────────────────────────
|
|
194
|
+
* 근거가 된 판정(`specId`)을 가리킬 수 있지만 **필수가 아니다** — 판정 없이 현장 재량으로 빼는 일이
|
|
195
|
+
* 정상이다. 그래서 정체성은 처분 대상(`subjectId`)이다.
|
|
196
|
+
*/
|
|
197
|
+
disposition: {
|
|
198
|
+
eventType: OP_EVENT.disposition,
|
|
199
|
+
identity: 'subjectId',
|
|
200
|
+
/* 무엇을 어떻게 하기로 했나 — 둘 중 하나가 없으면 그 결정은 아무 데도 붙지 못한다. */
|
|
201
|
+
required: ['subjectId', 'decision'],
|
|
202
|
+
fields: {
|
|
203
|
+
subjectId: 'string', decision: 'string', quantity: 'number', uom: 'string',
|
|
204
|
+
specId: 'string', decidedBy: 'string', reason: 'string', decidedAt: 'string', recordTime: 'string'
|
|
205
|
+
},
|
|
206
|
+
enums: { decision: DISPOSITION_DECISION }
|
|
207
|
+
},
|
|
133
208
|
test: {
|
|
134
209
|
eventType: OP_EVENT.test,
|
|
135
210
|
identity: 'testableObjectId',
|
|
@@ -197,6 +272,12 @@ export function operationalKindOf(record) {
|
|
|
197
272
|
return undefined;
|
|
198
273
|
const has = (k) => typeof r[k] === 'string' && r[k].trim().length > 0;
|
|
199
274
|
/* vocabulary-guard: allow 저널 와이어 필드로 가른다 */
|
|
275
|
+
/*
|
|
276
|
+
* 구간이 있으면 **마감된 상태 구간**이지 시점의 전이가 아니다. 전이보다 먼저 본다 — 뒤에 두면
|
|
277
|
+
* 사람이 되돌아가 적은 구간이 「지금 이 상태다」로 읽혀 트윈의 상태가 과거로 끌린다.
|
|
278
|
+
*/
|
|
279
|
+
if (has('moverId') && has('from') && has('to'))
|
|
280
|
+
return 'equipment-period';
|
|
200
281
|
if (has('moverId'))
|
|
201
282
|
return r.good !== undefined ? 'quality' : 'equipment';
|
|
202
283
|
if (has('personId'))
|
|
@@ -221,6 +302,15 @@ export function operationalKindOf(record) {
|
|
|
221
302
|
*/
|
|
222
303
|
if (has('testableObjectId'))
|
|
223
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';
|
|
224
314
|
/*
|
|
225
315
|
* ── ★ **채널을 열고 또 길을 내지 않았다** (2026-08-24) ──────────────────────
|
|
226
316
|
* `OP_EVENT.observation`(`location.measured`)을 내고 상태(`LocationState.observations`)와 조회
|
|
@@ -354,12 +444,28 @@ export function ingestOperationalRecords(records, opts) {
|
|
|
354
444
|
}
|
|
355
445
|
}
|
|
356
446
|
}
|
|
357
|
-
|
|
447
|
+
/*
|
|
448
|
+
* **마감된 구간은 구간의 끝에 성립한다.** 사람이 되돌아가 적는 사실이라 `at` 을 쓰면 「적은 때」가
|
|
449
|
+
* 사건 시각이 되고, 그러면 어제 있었던 정지가 오늘 일어난 것으로 저널에 적힌다. 적은 때는
|
|
450
|
+
* `recordTime` 에 남는다.
|
|
451
|
+
*/
|
|
452
|
+
const closedPeriodEnd = kind === 'equipment-period' ? String(r.to ?? '').trim() : '';
|
|
453
|
+
const at = closedPeriodEnd || String(r.at ?? '').trim() || opts.defaultEventTime;
|
|
358
454
|
const atMs = at ? Date.parse(at) : Number.NaN;
|
|
359
455
|
if (!Number.isFinite(atMs)) {
|
|
360
456
|
/* 시각이 없으면 순서를 판정할 수 없다 — 늦게 온 옛 사실이 최신 상태를 덮어써 위치가 과거로 튄다. */
|
|
361
457
|
errors.push(`${kind}: at 없음/형식 오류 — 시각 없이는 늦게 온 옛 사실을 걸러낼 수 없다`);
|
|
362
458
|
}
|
|
459
|
+
/*
|
|
460
|
+
* 아직 오지 않은 구간은 마감된 사실이 아니다 — 에너지의 마감된 구간과 같은 규칙이다. 받으면
|
|
461
|
+
* 성과 계산의 구간이 미래로 밀리고, 그 현장의 화면이 오류 없이 비어 보인다(2026-08-30 실측).
|
|
462
|
+
*/
|
|
463
|
+
if (closedPeriodEnd && opts.defaultEventTime) {
|
|
464
|
+
const nowMs = Date.parse(opts.defaultEventTime);
|
|
465
|
+
if (Number.isFinite(nowMs) && Number.isFinite(atMs) && atMs > nowMs) {
|
|
466
|
+
errors.push(`${kind}: 아직 오지 않은 시각의 사실이다(${closedPeriodEnd}) — 끝나지 않은 구간은 마감된 사실이 아니다`);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
363
469
|
if (errors.length) {
|
|
364
470
|
rejected.push({ record, errors });
|
|
365
471
|
continue;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const CONTRACT_VERSION = "0.4.0";
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 이 계약의 **판**.
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 내보내나 (2026-08-30) ─────────────────────────────────────────────
|
|
5
|
+
* 소비처가 「내가 어느 판의 계약을 지키고 있나」를 사실로 남겨야 한다. MES 의 아웃박스가 그 값을
|
|
6
|
+
* 레코드에 싣는데, 계약이 내보내지 않아 **상수로 손으로 적고 있었다.** `exports` 맵이 `package.json`
|
|
7
|
+
* 을 막아 런타임에 읽을 수도 없다.
|
|
8
|
+
*
|
|
9
|
+
* 손으로 적은 판은 계약이 올라갈 때 같이 올라가지 않는다. 그러면 그 값은 「지킨다고 말한 판」이지
|
|
10
|
+
* 「실제로 지킨 판」이 아니게 되고, 그것이 어긋난 날 아무도 모른다.
|
|
11
|
+
*
|
|
12
|
+
* **정본은 `package.json` 이다.** 이 값이 그것과 같은지는 시험이 지킨다(§`version-matches`).
|
|
13
|
+
*/
|
|
14
|
+
export const CONTRACT_VERSION = '0.4.0';
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -27,6 +27,7 @@ __export(index_exports, {
|
|
|
27
27
|
CAPABILITY_KEYS: () => CAPABILITY_KEYS,
|
|
28
28
|
CBV_BIZSTEP: () => CBV_BIZSTEP,
|
|
29
29
|
CMD: () => CMD,
|
|
30
|
+
CONTRACT_VERSION: () => CONTRACT_VERSION,
|
|
30
31
|
DISP: () => DISP,
|
|
31
32
|
DISPOSITION_DECISION: () => DISPOSITION_DECISION,
|
|
32
33
|
DOMAIN_CATALOG: () => DOMAIN_CATALOG,
|
|
@@ -774,6 +775,15 @@ var OP_EVENT = {
|
|
|
774
775
|
* 이 채널이 없으면 시험 결과는 **상태에만 있는 축**이 된다 — 재기동에서 사라지고, 폴드가 되살릴 수
|
|
775
776
|
* 없고, 미러가 이어받지 못한다(§상태 ⊆ 이벤트).
|
|
776
777
|
*/
|
|
778
|
+
/**
|
|
779
|
+
* **마감된 설비 상태 구간** — 이 설비가 이 구간 동안 이 상태였다.
|
|
780
|
+
*
|
|
781
|
+
* 시점의 전이(`equipment.status`)와 다른 사실이다. 전이는 설비가 내고, 이것은 **사람이 나중에
|
|
782
|
+
* 적는다.** 자재 대기처럼 신호를 내지 않는 정지는 이 길로만 들어온다.
|
|
783
|
+
*
|
|
784
|
+
* ISO 22400-2 의 가동률 계산이 이 구간들을 읽는다.
|
|
785
|
+
*/
|
|
786
|
+
equipmentPeriod: "equipment.state.period",
|
|
777
787
|
test: "test.result",
|
|
778
788
|
/**
|
|
779
789
|
* **부적합 처분** — 재고 판정을 받은 것을 어떻게 하기로 정했나(재작업 · 특채 · 폐기 · 반품).
|
|
@@ -3148,7 +3158,7 @@ function readEpochMs(raw) {
|
|
|
3148
3158
|
|
|
3149
3159
|
// src/operational-ingest.ts
|
|
3150
3160
|
var TASK_STATUS = ["created", "assigned", "in-progress", "completed"];
|
|
3151
|
-
var EQUIPMENT_STATUS = ["idle", "busy", "down"];
|
|
3161
|
+
var EQUIPMENT_STATUS = ["idle", "busy", "down", "setup", "planned-stop"];
|
|
3152
3162
|
var PERSON_STATUS = ["idle", "busy"];
|
|
3153
3163
|
var ASSET_STATUS = ["idle", "in-use"];
|
|
3154
3164
|
var SPECS = {
|
|
@@ -3206,6 +3216,8 @@ var SPECS = {
|
|
|
3206
3216
|
effectiveStart: "string",
|
|
3207
3217
|
effectiveEnd: "string",
|
|
3208
3218
|
recordTime: "string",
|
|
3219
|
+
/* 왜 이 상태가 됐나 — 설비가 알려 주면 싣는다. 사람이 나중에 정하는 사유는 구간 사실에 적는다. */
|
|
3220
|
+
reasonCode: "string",
|
|
3209
3221
|
/* 이동 구간 — 실 시스템도 줄 수 있는 사실이다(AGV·RTLS 가 출발·도착·소요를 낸다). 안쪽 필드까지
|
|
3210
3222
|
재검사하지는 않는다: 그 모양은 `EquipmentMotion` 계약이고, 여기서 두 번 지키면 두 벌이 된다. */
|
|
3211
3223
|
motion: "object"
|
|
@@ -3295,6 +3307,82 @@ var SPECS = {
|
|
|
3295
3307
|
* `propertyMeasurements` 안쪽은 재검사하지 않는다 — 그 모양은 `PropertyMeasurement` 계약이고,
|
|
3296
3308
|
* 여기서 두 번 지키면 두 벌이 된다(설비 `motion` 과 같은 규율).
|
|
3297
3309
|
*/
|
|
3310
|
+
/*
|
|
3311
|
+
* **마감된 설비 상태 구간** — 이 설비가 이 구간 동안 이 상태였다.
|
|
3312
|
+
*
|
|
3313
|
+
* ── 전이만으로는 안 되는 이유 (2026-08-30) ───────────────────────────────
|
|
3314
|
+
* 세 가지다. 셋째가 결정적이다.
|
|
3315
|
+
*
|
|
3316
|
+
* 사유가 붙을 자리가 없다 기계가 서는 순간에는 왜 섰는지 아무도 모른다. 작업자가 라인이 다시
|
|
3317
|
+
* 돈 뒤에 적는다. 그 사이에 전이가 더 있으면 어느 전이에 붙일지 정할 수 없다
|
|
3318
|
+
* 계획·비계획을 나중에 정한다 ISO 22400 은 계획정지를 계획 조업 시간에서 빼고 고장은 빼지 않는다.
|
|
3319
|
+
* 그 분류는 전이가 일어난 순간에 모른다
|
|
3320
|
+
* 전이를 내지 않는 정지가 많다 자재 대기 · 앞 공정 대기 · 작업자 부재. 기계는 전원이 켜진 채 `idle`
|
|
3321
|
+
* 이고 신호가 하나도 안 나온다. OEE 에서 가장 크게 깎이는 것이 보통 이 시간이다
|
|
3322
|
+
*
|
|
3323
|
+
* 마지막이 감시 시스템과 MES 가 갈리는 자리이기도 하다. 설비가 말하는 것만 모으면 감시이고,
|
|
3324
|
+
* **설비가 말하지 못하는 것을 사람이 적어 넣는 자리**가 MES 다.
|
|
3325
|
+
*
|
|
3326
|
+
* ── 이름이 `downtime` 이 아닌 이유 ────────────────────────────────────────
|
|
3327
|
+
* 이 구간은 `setup` 과 `planned-stop` 도 나른다. 그 둘은 정지가 아니다. 담는 것은 **상태 구간**이고,
|
|
3328
|
+
* 어느 상태인지는 `status` 가 말한다.
|
|
3329
|
+
*
|
|
3330
|
+
* ── 되돌아가 적는 사실이다 ───────────────────────────────────────────────
|
|
3331
|
+
* 봉투의 사건 시각은 **구간의 끝**(그때 성립한다)이고, 적은 시각은 `recordTime` 이다. 둘이 갈려 있어야
|
|
3332
|
+
* 「언제 일어났나」와 「언제 알았나」를 구별할 수 있다.
|
|
3333
|
+
*/
|
|
3334
|
+
"equipment-period": {
|
|
3335
|
+
eventType: OP_EVENT.equipmentPeriod,
|
|
3336
|
+
identity: "moverId",
|
|
3337
|
+
// vocabulary-guard: allow 저널 와이어 필드 — 전이와 같은 이름을 쓴다
|
|
3338
|
+
required: ["moverId", "status", "from", "to"],
|
|
3339
|
+
// vocabulary-guard: allow 위와 같은 이유
|
|
3340
|
+
fields: {
|
|
3341
|
+
moverId: "string",
|
|
3342
|
+
status: "string",
|
|
3343
|
+
from: "string",
|
|
3344
|
+
to: "string",
|
|
3345
|
+
// vocabulary-guard: allow
|
|
3346
|
+
/* 사람이 정한 사유 — 없을 수 있다(적지 않은 것과 사유가 없는 것은 다르므로 지어내지 않는다). */
|
|
3347
|
+
reasonCode: "string",
|
|
3348
|
+
decidedBy: "string",
|
|
3349
|
+
recordTime: "string"
|
|
3350
|
+
},
|
|
3351
|
+
enums: { status: EQUIPMENT_STATUS }
|
|
3352
|
+
},
|
|
3353
|
+
/*
|
|
3354
|
+
* **부적합 처분** — 재고 판정을 받은 것을 어떻게 하기로 정했나.
|
|
3355
|
+
*
|
|
3356
|
+
* ── 이 통이 늦게 생긴 이유 (2026-08-30) ──────────────────────────────────
|
|
3357
|
+
* 사건 이름(`OP_EVENT.disposition`)과 모양(`DispositionFact`)을 먼저 만들고 **여기로 오는 길을
|
|
3358
|
+
* 내지 않았다.** 그래서 처분 사실을 보내면 어느 통도 아니어서 떨어진 목록에도 남지 않고 사라졌다.
|
|
3359
|
+
* MES 전문가가 소스를 읽다 찾았다.
|
|
3360
|
+
*
|
|
3361
|
+
* 이 파일의 위 주석이 말한 부류 그대로다 — `OP_EVENT.test` 와 `OP_EVENT.observation` 이 같은 일을
|
|
3362
|
+
* 겪었고 이번이 셋째다. **계약에 자리를 만드는 것과 그 자리로 가는 길을 내는 것은 다른 일이다.**
|
|
3363
|
+
*
|
|
3364
|
+
* ── 판정과 다른 사실이다 ─────────────────────────────────────────────────
|
|
3365
|
+
* 근거가 된 판정(`specId`)을 가리킬 수 있지만 **필수가 아니다** — 판정 없이 현장 재량으로 빼는 일이
|
|
3366
|
+
* 정상이다. 그래서 정체성은 처분 대상(`subjectId`)이다.
|
|
3367
|
+
*/
|
|
3368
|
+
disposition: {
|
|
3369
|
+
eventType: OP_EVENT.disposition,
|
|
3370
|
+
identity: "subjectId",
|
|
3371
|
+
/* 무엇을 어떻게 하기로 했나 — 둘 중 하나가 없으면 그 결정은 아무 데도 붙지 못한다. */
|
|
3372
|
+
required: ["subjectId", "decision"],
|
|
3373
|
+
fields: {
|
|
3374
|
+
subjectId: "string",
|
|
3375
|
+
decision: "string",
|
|
3376
|
+
quantity: "number",
|
|
3377
|
+
uom: "string",
|
|
3378
|
+
specId: "string",
|
|
3379
|
+
decidedBy: "string",
|
|
3380
|
+
reason: "string",
|
|
3381
|
+
decidedAt: "string",
|
|
3382
|
+
recordTime: "string"
|
|
3383
|
+
},
|
|
3384
|
+
enums: { decision: DISPOSITION_DECISION }
|
|
3385
|
+
},
|
|
3298
3386
|
test: {
|
|
3299
3387
|
eventType: OP_EVENT.test,
|
|
3300
3388
|
identity: "testableObjectId",
|
|
@@ -3358,12 +3446,14 @@ function operationalKindOf(record) {
|
|
|
3358
3446
|
const r = record;
|
|
3359
3447
|
if (r.epc !== void 0 || r.meterId !== void 0 || r.equipmentId !== void 0) return void 0;
|
|
3360
3448
|
const has = (k) => typeof r[k] === "string" && r[k].trim().length > 0;
|
|
3449
|
+
if (has("moverId") && has("from") && has("to")) return "equipment-period";
|
|
3361
3450
|
if (has("moverId")) return r.good !== void 0 ? "quality" : "equipment";
|
|
3362
3451
|
if (has("personId")) return "person";
|
|
3363
3452
|
if (has("assetId")) return "asset";
|
|
3364
3453
|
if (has("taskId")) return "task";
|
|
3365
3454
|
if (has("orderId")) return "order";
|
|
3366
3455
|
if (has("testableObjectId")) return "test";
|
|
3456
|
+
if (has("subjectId") && has("decision")) return "disposition";
|
|
3367
3457
|
if (has("locationId") && has("propertyId")) return "observation";
|
|
3368
3458
|
if (has("completeAxis")) return "complete";
|
|
3369
3459
|
return void 0;
|
|
@@ -3465,11 +3555,18 @@ function ingestOperationalRecords(records, opts) {
|
|
|
3465
3555
|
}
|
|
3466
3556
|
}
|
|
3467
3557
|
}
|
|
3468
|
-
const
|
|
3558
|
+
const closedPeriodEnd = kind === "equipment-period" ? String(r.to ?? "").trim() : "";
|
|
3559
|
+
const at = closedPeriodEnd || String(r.at ?? "").trim() || opts.defaultEventTime;
|
|
3469
3560
|
const atMs = at ? Date.parse(at) : Number.NaN;
|
|
3470
3561
|
if (!Number.isFinite(atMs)) {
|
|
3471
3562
|
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`);
|
|
3472
3563
|
}
|
|
3564
|
+
if (closedPeriodEnd && opts.defaultEventTime) {
|
|
3565
|
+
const nowMs = Date.parse(opts.defaultEventTime);
|
|
3566
|
+
if (Number.isFinite(nowMs) && Number.isFinite(atMs) && atMs > nowMs) {
|
|
3567
|
+
errors.push(`${kind}: \uC544\uC9C1 \uC624\uC9C0 \uC54A\uC740 \uC2DC\uAC01\uC758 \uC0AC\uC2E4\uC774\uB2E4(${closedPeriodEnd}) \u2014 \uB05D\uB098\uC9C0 \uC54A\uC740 \uAD6C\uAC04\uC740 \uB9C8\uAC10\uB41C \uC0AC\uC2E4\uC774 \uC544\uB2C8\uB2E4`);
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3473
3570
|
if (errors.length) {
|
|
3474
3571
|
rejected.push({ record, errors });
|
|
3475
3572
|
continue;
|
|
@@ -3634,6 +3731,9 @@ function retiredVocabularyIn(line) {
|
|
|
3634
3731
|
}
|
|
3635
3732
|
return hits;
|
|
3636
3733
|
}
|
|
3734
|
+
|
|
3735
|
+
// src/version.ts
|
|
3736
|
+
var CONTRACT_VERSION = "0.4.0";
|
|
3637
3737
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3638
3738
|
0 && (module.exports = {
|
|
3639
3739
|
BIZSTEP,
|
|
@@ -3644,6 +3744,7 @@ function retiredVocabularyIn(line) {
|
|
|
3644
3744
|
CAPABILITY_KEYS,
|
|
3645
3745
|
CBV_BIZSTEP,
|
|
3646
3746
|
CMD,
|
|
3747
|
+
CONTRACT_VERSION,
|
|
3647
3748
|
DISP,
|
|
3648
3749
|
DISPOSITION_DECISION,
|
|
3649
3750
|
DOMAIN_CATALOG,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@operato/ops-contract",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|