@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 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
- * 스토어 패키지 `@operato/twin-catalog` 타입을 import type { EffectivePeriod } from './contract.ts'
7
- import 스토어 메타(version/supplier)를 얹어 배포한다(catalog kernel 의존).
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
+ }
@@ -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";
@@ -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) 과 필드가 겹치지 않는다 — 여기는 `epc` + `status`. 상태 낱말은 열려
327
- * 있어 enum 없다(오더 상태와 같은 규율). 로트 전체의 사실이므로 `subLotId` 받지 않는다 부분마다 다른
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: ['epc', 'status'],
334
+ match: ['lotId', 'status'],
333
335
  matchOrder: 85,
334
- identity: 'epc',
335
- required: ['epc', 'status'],
336
- fields: { epc: 'string', status: 'string', decidedBy: 'string', reason: 'string', decidedAt: 'string', recordTime: 'string' }
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];
@@ -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) 과 필드가 겹치지 않는다 — 여기는 `epc` + `status`. 상태 낱말은 열려
3717
- * 있어 enum 없다(오더 상태와 같은 규율). 로트 전체의 사실이므로 `subLotId` 받지 않는다 부분마다 다른
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: ["epc", "status"],
3729
+ match: ["lotId", "status"],
3723
3730
  matchOrder: 85,
3724
- identity: "epc",
3725
- required: ["epc", "status"],
3726
- fields: { epc: "string", status: "string", decidedBy: "string", reason: "string", decidedAt: "string", recordTime: "string" }
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.20",
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",