@operato/ops-contract 0.9.20 → 0.9.23
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.js +1 -1
- package/dist/domain-definition.d.ts +21 -0
- package/dist/domain-definition.js +16 -2
- package/dist/mes-profile.d.ts +0 -6
- package/dist/mes-profile.js +1 -7
- package/dist/operational-ingest.d.ts +6 -0
- package/dist/operational-ingest.js +24 -11
- package/dist-cjs/index.cjs +32 -19
- package/package.json +1 -1
package/dist/contract.js
CHANGED
|
@@ -1112,7 +1112,7 @@ export const OP_EVENT = {
|
|
|
1112
1112
|
* 승인은 자재를 움직이지 않는다. 자재가 트윈 밖으로 나가는 것은 EPCIS `shipping` 사건이다. 둘을 한 사건으로
|
|
1113
1113
|
* 접지 않는다 — 승인됐는데 안 나간 배치가 트윈이 보여야 할 상태다.
|
|
1114
1114
|
*/
|
|
1115
|
-
materialLot: 'material-lot.status',
|
|
1115
|
+
materialLot: 'material-lot.status', // 레코드: { lotId(표준 MaterialLotID = ItemState.epc 의 값), status, decidedBy?, reason?, decidedAt? }
|
|
1116
1116
|
/**
|
|
1117
1117
|
* **이 목록이 전부다** — 연결된 시스템이 현재 목록을 한 바퀴 다 보낸 뒤 그것을 알린다.
|
|
1118
1118
|
*
|
|
@@ -374,3 +374,24 @@ export interface DomainDefinition {
|
|
|
374
374
|
* 도메인 정의는 데이터 아티팩트라 로드 시점에 런타임 검증한다(컴파일타임 아님).
|
|
375
375
|
*/
|
|
376
376
|
export declare function validateDomainDefinition(def: DomainDefinition): string[];
|
|
377
|
+
export interface DomainCatalog extends DomainDefinition {
|
|
378
|
+
/** semver. 같은 업종의 정의가 고쳐지면 이것이 오른다. */
|
|
379
|
+
version: string;
|
|
380
|
+
supplier?: string;
|
|
381
|
+
standards?: string[];
|
|
382
|
+
description?: string;
|
|
383
|
+
}
|
|
384
|
+
/** 스토어 목록·발견용 요약 — 정의 전체를 열지 않고 고르게 한다. */
|
|
385
|
+
export interface CatalogSummary {
|
|
386
|
+
id: string;
|
|
387
|
+
version: string;
|
|
388
|
+
label: string;
|
|
389
|
+
supplier?: string;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* 배포 아티팩트 검증 = 정의 검증 + 판 번호.
|
|
393
|
+
*
|
|
394
|
+
* 정의의 참조무결성은 `validateDomainDefinition` 이 소유한다 — 여기서 다시 쓰지 않는다. 위반 목록을
|
|
395
|
+
* 돌려주고, 빈 배열이 유효다.
|
|
396
|
+
*/
|
|
397
|
+
export declare function validateCatalog(cat: DomainCatalog): string[];
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* 커널이 "무엇이 있고 어떻게 흐르나"(타입 + 공정 route/BOM)를 **데이터로** 받는 형식. zero-dep(자기 타입).
|
|
4
4
|
*
|
|
5
5
|
* 특정 공정이 커널 코드에 하드코딩되던 것을 이 데이터 계약으로 대체한다(design/plans/domain-catalog-layering.md).
|
|
6
|
-
*
|
|
7
|
-
|
|
6
|
+
* 판을 붙여 주고받는 꼴은 이 파일 아래쪽의 `DomainCatalog` 다 — 전에는 `@operato/twin-catalog` 라는
|
|
7
|
+
* 별도 패키지였고, 2026-09-17 에 여기로 접었다.
|
|
8
8
|
* 표준 앵커: GS1 EPCIS 2.0(bizStep) · ISA-95(WorkCenter·OperationsDefinition·BOM) · ISO 55000(Asset).
|
|
9
9
|
*/
|
|
10
10
|
/**
|
|
@@ -136,3 +136,17 @@ export function validateDomainDefinition(def) {
|
|
|
136
136
|
}
|
|
137
137
|
return v;
|
|
138
138
|
}
|
|
139
|
+
const CATALOG_SEMVER = /^\d+\.\d+\.\d+([-+].+)?$/;
|
|
140
|
+
/**
|
|
141
|
+
* 배포 아티팩트 검증 = 정의 검증 + 판 번호.
|
|
142
|
+
*
|
|
143
|
+
* 정의의 참조무결성은 `validateDomainDefinition` 이 소유한다 — 여기서 다시 쓰지 않는다. 위반 목록을
|
|
144
|
+
* 돌려주고, 빈 배열이 유효다.
|
|
145
|
+
*/
|
|
146
|
+
export function validateCatalog(cat) {
|
|
147
|
+
const v = validateDomainDefinition(cat);
|
|
148
|
+
if (typeof cat?.version !== 'string' || !CATALOG_SEMVER.test(cat?.version || '')) {
|
|
149
|
+
v.push(`version semver 아님: ${cat?.version}`);
|
|
150
|
+
}
|
|
151
|
+
return v;
|
|
152
|
+
}
|
package/dist/mes-profile.d.ts
CHANGED
|
@@ -4,12 +4,6 @@ export declare const MES_BIZSTEP: {
|
|
|
4
4
|
readonly receiving: "urn:epcglobal:cbv:bizstep:receiving";
|
|
5
5
|
readonly producing: "urn:epcglobal:cbv:bizstep:commissioning";
|
|
6
6
|
readonly storing: "urn:epcglobal:cbv:bizstep:storing";
|
|
7
|
-
/**
|
|
8
|
-
* **출하** — CBV `shipping`: 물품이 시설을 떠난다. 자재를 트윈 밖으로 내는 것은 이 사건이다(disposition
|
|
9
|
-
* `in_transit`, readPoint = 출하 dock). 출하 **승인**은 이 사건이 아니라 로트 상태(`OP_EVENT.materialLot`)다 —
|
|
10
|
-
* 승인됐는데 안 나간 배치가 보여야 한다(2026-09-16, ADR-0046 곁).
|
|
11
|
-
*/
|
|
12
|
-
readonly shipping: "urn:epcglobal:cbv:bizstep:shipping";
|
|
13
7
|
};
|
|
14
8
|
/** 작업지시(Work Order) = 생산 오더 거래 유형. */
|
|
15
9
|
export declare const BTT_PRODORDER = "urn:epcglobal:cbv:btt:prodorder";
|
package/dist/mes-profile.js
CHANGED
|
@@ -2,13 +2,7 @@
|
|
|
2
2
|
export const MES_BIZSTEP = {
|
|
3
3
|
receiving: 'urn:epcglobal:cbv:bizstep:receiving', // 원자재 수령
|
|
4
4
|
producing: 'urn:epcglobal:cbv:bizstep:commissioning', // 생산(제품 최초 생성)
|
|
5
|
-
storing: 'urn:epcglobal:cbv:bizstep:storing'
|
|
6
|
-
/**
|
|
7
|
-
* **출하** — CBV `shipping`: 물품이 시설을 떠난다. 자재를 트윈 밖으로 내는 것은 이 사건이다(disposition
|
|
8
|
-
* `in_transit`, readPoint = 출하 dock). 출하 **승인**은 이 사건이 아니라 로트 상태(`OP_EVENT.materialLot`)다 —
|
|
9
|
-
* 승인됐는데 안 나간 배치가 보여야 한다(2026-09-16, ADR-0046 곁).
|
|
10
|
-
*/
|
|
11
|
-
shipping: 'urn:epcglobal:cbv:bizstep:shipping'
|
|
5
|
+
storing: 'urn:epcglobal:cbv:bizstep:storing' // 완제품 저장
|
|
12
6
|
};
|
|
13
7
|
/** 작업지시(Work Order) = 생산 오더 거래 유형. */
|
|
14
8
|
export const BTT_PRODORDER = 'urn:epcglobal:cbv:btt:prodorder';
|
|
@@ -65,6 +65,12 @@ export interface ObjectShape {
|
|
|
65
65
|
* 거부했다. 자원(설비·사람·자산)을 먼저 보고, 작업을 오더보다 먼저 본다.
|
|
66
66
|
*/
|
|
67
67
|
export declare function operationalKindOf(record: unknown): OperationalKind | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* 어느 갈래도 아닐 때의 거절 문장 — **표에서 세어 만든다.** 손으로 적은 목록은 다섯에서 멈춰 있었다(kind 는 열둘).
|
|
70
|
+
* 그 문장은 보내는 쪽이 실제로 읽는 것이라, 빠진 갈래는 「애초에 못 보내는 것」으로 읽힌다(2026-09-16, 인티그레이션
|
|
71
|
+
* 레인 측정). 여기서 만들면 kind 가 늘 때 문장도 함께 는다.
|
|
72
|
+
*/
|
|
73
|
+
export declare function unknownKindReason(): string;
|
|
68
74
|
/** 이 레코드가 운영 사실인가 — 호스트의 라우팅이 묻는 자리. */
|
|
69
75
|
/**
|
|
70
76
|
* 그 종류의 사실을 **무엇이 가리키나** — 선언된 identity 칸의 이름.
|
|
@@ -323,17 +323,19 @@ const SPECS = {
|
|
|
323
323
|
/*
|
|
324
324
|
* **로트 상태** — ISA-95 `MaterialLot.Status`. 출하 승인(batch release)이 들어오는 문(§`OP_EVENT.materialLot`).
|
|
325
325
|
*
|
|
326
|
-
* `disposition`(subjectId + decision) 과 필드가 겹치지 않는다 — 여기는 `
|
|
327
|
-
*
|
|
328
|
-
*
|
|
326
|
+
* `disposition`(subjectId + decision) 과 필드가 겹치지 않는다 — 여기는 `lotId` + `status`. 정체 이름이 표준
|
|
327
|
+
* `MaterialLotID` 인 이유가 하나 더 있다: 이 문은 `epc` 를 든 레코드를 EPCIS 어휘로 보고 받지 않는다
|
|
328
|
+
* (§`operationalKindOf`). 값은 로트의 식별자(`ItemState.epc` 와 같은 값)다. 상태 낱말은 열려 있어 enum 이
|
|
329
|
+
* 없다(오더 상태와 같은 규율). 로트 전체의 사실이므로 부분(`subLotId`)은 받지 않는다 — 부분마다 다른 상태가
|
|
330
|
+
* 필요해지면 그것은 다른 사실이다.
|
|
329
331
|
*/
|
|
330
332
|
'material-lot': {
|
|
331
333
|
eventType: OP_EVENT.materialLot,
|
|
332
|
-
match: ['
|
|
334
|
+
match: ['lotId', 'status'],
|
|
333
335
|
matchOrder: 85,
|
|
334
|
-
identity: '
|
|
335
|
-
required: ['
|
|
336
|
-
fields: {
|
|
336
|
+
identity: 'lotId',
|
|
337
|
+
required: ['lotId', 'status'],
|
|
338
|
+
fields: { lotId: 'string', status: 'string', decidedBy: 'string', reason: 'string', decidedAt: 'string', recordTime: 'string' }
|
|
337
339
|
},
|
|
338
340
|
test: {
|
|
339
341
|
eventType: OP_EVENT.test,
|
|
@@ -425,6 +427,20 @@ export function operationalKindOf(record) {
|
|
|
425
427
|
}
|
|
426
428
|
return undefined;
|
|
427
429
|
}
|
|
430
|
+
/**
|
|
431
|
+
* 어느 갈래도 아닐 때의 거절 문장 — **표에서 세어 만든다.** 손으로 적은 목록은 다섯에서 멈춰 있었다(kind 는 열둘).
|
|
432
|
+
* 그 문장은 보내는 쪽이 실제로 읽는 것이라, 빠진 갈래는 「애초에 못 보내는 것」으로 읽힌다(2026-09-16, 인티그레이션
|
|
433
|
+
* 레인 측정). 여기서 만들면 kind 가 늘 때 문장도 함께 는다.
|
|
434
|
+
*/
|
|
435
|
+
export function unknownKindReason() {
|
|
436
|
+
const shapes = MATCH_ORDER.map(kind => {
|
|
437
|
+
const spec = SPECS[kind];
|
|
438
|
+
const fields = spec.match ?? [spec.identity];
|
|
439
|
+
return `${kind}: ${fields.join('+')}`;
|
|
440
|
+
});
|
|
441
|
+
// vocabulary-guard: allow 거부 이유가 계약 필드 이름을 말한다
|
|
442
|
+
return `어느 운영 사실인지 모른다 — 갈래를 정하는 필드가 필요하다(${shapes.join(' · ')}). epc 를 든 레코드는 EPCIS 문으로 간다`;
|
|
443
|
+
}
|
|
428
444
|
/**
|
|
429
445
|
* 판정 순서 — 표의 `matchOrder` 에서 한 번만 만든다.
|
|
430
446
|
*
|
|
@@ -471,10 +487,7 @@ export function ingestOperationalRecords(records, opts) {
|
|
|
471
487
|
for (const record of arr) {
|
|
472
488
|
const kind = operationalKindOf(record);
|
|
473
489
|
if (!kind) {
|
|
474
|
-
rejected.push({
|
|
475
|
-
record,
|
|
476
|
-
errors: ['어느 운영 사실인지 모른다 — 정체 필드가 필요하다(taskId · moverId(+good=품질) · personId · assetId · orderId)'] // vocabulary-guard: allow 거부 이유가 계약 필드 이름을 말한다
|
|
477
|
-
});
|
|
490
|
+
rejected.push({ record, errors: [unknownKindReason()] });
|
|
478
491
|
continue;
|
|
479
492
|
}
|
|
480
493
|
const spec = SPECS[kind];
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -230,6 +230,8 @@ __export(index_exports, {
|
|
|
230
230
|
transactionEvent: () => transactionEvent,
|
|
231
231
|
transformationEvent: () => transformationEvent,
|
|
232
232
|
unavailableVerdict: () => unavailableVerdict,
|
|
233
|
+
unknownKindReason: () => unknownKindReason,
|
|
234
|
+
validateCatalog: () => validateCatalog,
|
|
233
235
|
validateDomainDefinition: () => validateDomainDefinition,
|
|
234
236
|
validateEpcisEvent: () => validateEpcisEvent,
|
|
235
237
|
validatePerformance: () => validatePerformance,
|
|
@@ -925,6 +927,7 @@ var OP_EVENT = {
|
|
|
925
927
|
* 접지 않는다 — 승인됐는데 안 나간 배치가 트윈이 보여야 할 상태다.
|
|
926
928
|
*/
|
|
927
929
|
materialLot: "material-lot.status",
|
|
930
|
+
// 레코드: { lotId(표준 MaterialLotID = ItemState.epc 의 값), status, decidedBy?, reason?, decidedAt? }
|
|
928
931
|
/**
|
|
929
932
|
* **이 목록이 전부다** — 연결된 시스템이 현재 목록을 한 바퀴 다 보낸 뒤 그것을 알린다.
|
|
930
933
|
*
|
|
@@ -1316,14 +1319,8 @@ var MES_BIZSTEP = {
|
|
|
1316
1319
|
// 원자재 수령
|
|
1317
1320
|
producing: "urn:epcglobal:cbv:bizstep:commissioning",
|
|
1318
1321
|
// 생산(제품 최초 생성)
|
|
1319
|
-
storing: "urn:epcglobal:cbv:bizstep:storing"
|
|
1322
|
+
storing: "urn:epcglobal:cbv:bizstep:storing"
|
|
1320
1323
|
// 완제품 저장
|
|
1321
|
-
/**
|
|
1322
|
-
* **출하** — CBV `shipping`: 물품이 시설을 떠난다. 자재를 트윈 밖으로 내는 것은 이 사건이다(disposition
|
|
1323
|
-
* `in_transit`, readPoint = 출하 dock). 출하 **승인**은 이 사건이 아니라 로트 상태(`OP_EVENT.materialLot`)다 —
|
|
1324
|
-
* 승인됐는데 안 나간 배치가 보여야 한다(2026-09-16, ADR-0046 곁).
|
|
1325
|
-
*/
|
|
1326
|
-
shipping: "urn:epcglobal:cbv:bizstep:shipping"
|
|
1327
1324
|
};
|
|
1328
1325
|
var BTT_PRODORDER = "urn:epcglobal:cbv:btt:prodorder";
|
|
1329
1326
|
function sgtinUri(companyPrefix, itemRef, serial) {
|
|
@@ -2181,6 +2178,14 @@ function validateDomainDefinition(def) {
|
|
|
2181
2178
|
}
|
|
2182
2179
|
return v;
|
|
2183
2180
|
}
|
|
2181
|
+
var CATALOG_SEMVER = /^\d+\.\d+\.\d+([-+].+)?$/;
|
|
2182
|
+
function validateCatalog(cat) {
|
|
2183
|
+
const v = validateDomainDefinition(cat);
|
|
2184
|
+
if (typeof cat?.version !== "string" || !CATALOG_SEMVER.test(cat?.version || "")) {
|
|
2185
|
+
v.push(`version semver \uC544\uB2D8: ${cat?.version}`);
|
|
2186
|
+
}
|
|
2187
|
+
return v;
|
|
2188
|
+
}
|
|
2184
2189
|
|
|
2185
2190
|
// src/energy-ingest.ts
|
|
2186
2191
|
function isEnergyRecord(record) {
|
|
@@ -3713,17 +3718,19 @@ var SPECS = {
|
|
|
3713
3718
|
/*
|
|
3714
3719
|
* **로트 상태** — ISA-95 `MaterialLot.Status`. 출하 승인(batch release)이 들어오는 문(§`OP_EVENT.materialLot`).
|
|
3715
3720
|
*
|
|
3716
|
-
* `disposition`(subjectId + decision) 과 필드가 겹치지 않는다 — 여기는 `
|
|
3717
|
-
*
|
|
3718
|
-
*
|
|
3721
|
+
* `disposition`(subjectId + decision) 과 필드가 겹치지 않는다 — 여기는 `lotId` + `status`. 정체 이름이 표준
|
|
3722
|
+
* `MaterialLotID` 인 이유가 하나 더 있다: 이 문은 `epc` 를 든 레코드를 EPCIS 어휘로 보고 받지 않는다
|
|
3723
|
+
* (§`operationalKindOf`). 값은 로트의 식별자(`ItemState.epc` 와 같은 값)다. 상태 낱말은 열려 있어 enum 이
|
|
3724
|
+
* 없다(오더 상태와 같은 규율). 로트 전체의 사실이므로 부분(`subLotId`)은 받지 않는다 — 부분마다 다른 상태가
|
|
3725
|
+
* 필요해지면 그것은 다른 사실이다.
|
|
3719
3726
|
*/
|
|
3720
3727
|
"material-lot": {
|
|
3721
3728
|
eventType: OP_EVENT.materialLot,
|
|
3722
|
-
match: ["
|
|
3729
|
+
match: ["lotId", "status"],
|
|
3723
3730
|
matchOrder: 85,
|
|
3724
|
-
identity: "
|
|
3725
|
-
required: ["
|
|
3726
|
-
fields: {
|
|
3731
|
+
identity: "lotId",
|
|
3732
|
+
required: ["lotId", "status"],
|
|
3733
|
+
fields: { lotId: "string", status: "string", decidedBy: "string", reason: "string", decidedAt: "string", recordTime: "string" }
|
|
3727
3734
|
},
|
|
3728
3735
|
test: {
|
|
3729
3736
|
eventType: OP_EVENT.test,
|
|
@@ -3805,6 +3812,14 @@ function operationalKindOf(record) {
|
|
|
3805
3812
|
}
|
|
3806
3813
|
return void 0;
|
|
3807
3814
|
}
|
|
3815
|
+
function unknownKindReason() {
|
|
3816
|
+
const shapes = MATCH_ORDER.map((kind) => {
|
|
3817
|
+
const spec = SPECS[kind];
|
|
3818
|
+
const fields = spec.match ?? [spec.identity];
|
|
3819
|
+
return `${kind}: ${fields.join("+")}`;
|
|
3820
|
+
});
|
|
3821
|
+
return `\uC5B4\uB290 \uC6B4\uC601 \uC0AC\uC2E4\uC778\uC9C0 \uBAA8\uB978\uB2E4 \u2014 \uAC08\uB798\uB97C \uC815\uD558\uB294 \uD544\uB4DC\uAC00 \uD544\uC694\uD558\uB2E4(${shapes.join(" \xB7 ")}). epc \uB97C \uB4E0 \uB808\uCF54\uB4DC\uB294 EPCIS \uBB38\uC73C\uB85C \uAC04\uB2E4`;
|
|
3822
|
+
}
|
|
3808
3823
|
var MATCH_ORDER = Object.keys(SPECS).sort(
|
|
3809
3824
|
(a, b) => (SPECS[a].matchOrder ?? 1e3) - (SPECS[b].matchOrder ?? 1e3)
|
|
3810
3825
|
);
|
|
@@ -3822,11 +3837,7 @@ function ingestOperationalRecords(records, opts) {
|
|
|
3822
3837
|
for (const record of arr) {
|
|
3823
3838
|
const kind = operationalKindOf(record);
|
|
3824
3839
|
if (!kind) {
|
|
3825
|
-
rejected.push({
|
|
3826
|
-
record,
|
|
3827
|
-
errors: ["\uC5B4\uB290 \uC6B4\uC601 \uC0AC\uC2E4\uC778\uC9C0 \uBAA8\uB978\uB2E4 \u2014 \uC815\uCCB4 \uD544\uB4DC\uAC00 \uD544\uC694\uD558\uB2E4(taskId \xB7 moverId(+good=\uD488\uC9C8) \xB7 personId \xB7 assetId \xB7 orderId)"]
|
|
3828
|
-
// vocabulary-guard: allow 거부 이유가 계약 필드 이름을 말한다
|
|
3829
|
-
});
|
|
3840
|
+
rejected.push({ record, errors: [unknownKindReason()] });
|
|
3830
3841
|
continue;
|
|
3831
3842
|
}
|
|
3832
3843
|
const spec = SPECS[kind];
|
|
@@ -4790,6 +4801,8 @@ function nonEmpty(value) {
|
|
|
4790
4801
|
transactionEvent,
|
|
4791
4802
|
transformationEvent,
|
|
4792
4803
|
unavailableVerdict,
|
|
4804
|
+
unknownKindReason,
|
|
4805
|
+
validateCatalog,
|
|
4793
4806
|
validateDomainDefinition,
|
|
4794
4807
|
validateEpcisEvent,
|
|
4795
4808
|
validatePerformance,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@operato/ops-contract",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.23",
|
|
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",
|