@operato/ops-contract 0.9.3 → 0.9.4
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 +17 -2
- package/dist/contract.js +25 -0
- package/dist/epcis.d.ts +18 -0
- package/dist/epcis.js +76 -4
- package/dist/operational-ingest.d.ts +28 -0
- package/dist/operational-ingest.js +76 -1
- package/dist-cjs/index.cjs +81 -5
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -3269,11 +3269,26 @@ export interface QualityDelta {
|
|
|
3269
3269
|
* 표준 매핑은 커널이 이미 적어 두었다 — `MaterialDefinitionID` → `definitionId`,
|
|
3270
3270
|
* `MaterialUse` → `use`. 그 이름을 그대로 쓴다.
|
|
3271
3271
|
*/
|
|
3272
|
+
/**
|
|
3273
|
+
* **실적의 자재 쓰임** — 표준 `MaterialUse`. 들어갔나 나왔나 둘이다.
|
|
3274
|
+
*
|
|
3275
|
+
* 공정 **명세**의 쓰임(`OperationMaterial.use`)은 셋이다 — 거기에는 `consumable` 이 있다(쓰이지만
|
|
3276
|
+
* 제품에 남지 않는 것: 세척수·윤활유). **실적에는 그 낱말이 없다** — 일어난 일을 적는 자리이고,
|
|
3277
|
+
* 소모품이 실제로 들어간 것은 `consumed` 다.
|
|
3278
|
+
*
|
|
3279
|
+
* 둘을 한 어휘로 묶지 않는다. 묶으면 실적에 `consumable` 이 올 수 있게 되고, 그때 그것이
|
|
3280
|
+
* 「들어갔다」인지 「쓰였지만 안 남았다」인지 받는 쪽이 정해야 한다.
|
|
3281
|
+
*
|
|
3282
|
+
* 유입 게이트가 이 상수를 가리킨다(§`operational-ingest.ts` 의 `shapes`) — 낱말을 두 곳에 적으면
|
|
3283
|
+
* 한쪽만 늘어나는 날 게이트가 계약에 없는 낱말을 통과시킨다.
|
|
3284
|
+
*/
|
|
3285
|
+
export declare const MATERIAL_ACTUAL_USE: readonly ["consumed", "produced"];
|
|
3286
|
+
export type MaterialActualUse = (typeof MATERIAL_ACTUAL_USE)[number];
|
|
3272
3287
|
export interface MaterialActual {
|
|
3273
3288
|
/** 표준 `MaterialDefinitionID`. */
|
|
3274
3289
|
definitionId: string;
|
|
3275
|
-
/** 표준 `MaterialUse
|
|
3276
|
-
use:
|
|
3290
|
+
/** 표준 `MaterialUse` — 실적은 둘이다(§`MATERIAL_ACTUAL_USE`). */
|
|
3291
|
+
use: MaterialActualUse;
|
|
3277
3292
|
quantity: number;
|
|
3278
3293
|
uom?: string;
|
|
3279
3294
|
/**
|
package/dist/contract.js
CHANGED
|
@@ -1318,6 +1318,31 @@ export function isFailureStatus(value) {
|
|
|
1318
1318
|
export function isPlannedStopStatus(value) {
|
|
1319
1319
|
return normalizeEquipmentStatus(value) === 'planned-stop';
|
|
1320
1320
|
}
|
|
1321
|
+
/**
|
|
1322
|
+
* 실제로 들어가고 나온 자재 한 줄 — ISA-95 `OpMaterialActualType` 의 부분집합.
|
|
1323
|
+
*
|
|
1324
|
+
* ── 왜 이름 있는 타입인가 (2026-08-31) ────────────────────────────────────
|
|
1325
|
+
* 같은 데이터가 네 곳에 각각 인라인으로 적혀 있었다(작업 상태 둘 · 커널 · ERP). ERP 쪽은 필드명까지
|
|
1326
|
+
* 달랐다(`materialDefinitionId` · `direction`). 같은 데이터를 여러 이름으로 부르면 옮기는 코드가
|
|
1327
|
+
* 생기고, 그 코드가 언젠가 어긋난다.
|
|
1328
|
+
*
|
|
1329
|
+
* 표준 매핑은 커널이 이미 적어 두었다 — `MaterialDefinitionID` → `definitionId`,
|
|
1330
|
+
* `MaterialUse` → `use`. 그 이름을 그대로 쓴다.
|
|
1331
|
+
*/
|
|
1332
|
+
/**
|
|
1333
|
+
* **실적의 자재 쓰임** — 표준 `MaterialUse`. 들어갔나 나왔나 둘이다.
|
|
1334
|
+
*
|
|
1335
|
+
* 공정 **명세**의 쓰임(`OperationMaterial.use`)은 셋이다 — 거기에는 `consumable` 이 있다(쓰이지만
|
|
1336
|
+
* 제품에 남지 않는 것: 세척수·윤활유). **실적에는 그 낱말이 없다** — 일어난 일을 적는 자리이고,
|
|
1337
|
+
* 소모품이 실제로 들어간 것은 `consumed` 다.
|
|
1338
|
+
*
|
|
1339
|
+
* 둘을 한 어휘로 묶지 않는다. 묶으면 실적에 `consumable` 이 올 수 있게 되고, 그때 그것이
|
|
1340
|
+
* 「들어갔다」인지 「쓰였지만 안 남았다」인지 받는 쪽이 정해야 한다.
|
|
1341
|
+
*
|
|
1342
|
+
* 유입 게이트가 이 상수를 가리킨다(§`operational-ingest.ts` 의 `shapes`) — 낱말을 두 곳에 적으면
|
|
1343
|
+
* 한쪽만 늘어나는 날 게이트가 계약에 없는 낱말을 통과시킨다.
|
|
1344
|
+
*/
|
|
1345
|
+
export const MATERIAL_ACTUAL_USE = ['consumed', 'produced'];
|
|
1321
1346
|
// ── Command 채널 어휘 — 트윈의 "행위(act)" 면 (prescriptive/트랜잭션 프론트엔드) ──
|
|
1322
1347
|
// 코어 공통: order.hold/resume(할당 보류). 도메인: order.release(즉시 투입) 등은 handleCommand 로.
|
|
1323
1348
|
export const CMD = {
|
package/dist/epcis.d.ts
CHANGED
|
@@ -385,6 +385,24 @@ export declare function gdtiUri(companyPrefix: string, docType: string, serial:
|
|
|
385
385
|
* Link 를 **권한다**(SHOULD) — 이 길은 프리픽스가 없을 때의 정합 경로다.
|
|
386
386
|
*/
|
|
387
387
|
export declare function objectUri(namespace: string, objId: string | number): string | undefined;
|
|
388
|
+
/**
|
|
389
|
+
* **선언된 이름공간 아래의 클래스 식별자** — SGTIN·LGTIN 을 쓰지 않는 길.
|
|
390
|
+
*
|
|
391
|
+
* 품목 등급·로트 등급처럼 「낱개가 아니라 종류」를 가리키는 자리다. 개체 쪽(`objectUri`)과 같은
|
|
392
|
+
* 사정으로 필요하다: GTIN 을 조립하려면 회사 프리픽스가 있어야 하고, 없는 현장이 그 길로 못 간다.
|
|
393
|
+
*
|
|
394
|
+
* · CBV 2.0 §8.3.4 `http(s)://[Subdomain.]Domain/⁎⁎/class/ClassID`
|
|
395
|
+
* · CBV 2.0 §8.3.3 `urn:URNNamespace:⁎⁎:class:ClassID`
|
|
396
|
+
*
|
|
397
|
+
* ── 왜 계약에 두나 (2026-09-04, plant 레인이 올림) ─────────────────────────
|
|
398
|
+
* 체프 커넥터가 이 URI 를 **직접 조립하고 있었다** — `productClassId` · `lotClassId` 라는 자기
|
|
399
|
+
* 이름으로. 그중 `bizTransactionId` 는 이 파일의 `bizTransactionUri` 와 **같은 것을 다르게**
|
|
400
|
+
* 만들고 있었다. `underNamespace` 는 처음부터 `'class'` 표지를 알고 있었고(그 함수 주석에
|
|
401
|
+
* §8.3.3·§8.3.4 까지 적혀 있다) **부를 문만 없었다.**
|
|
402
|
+
*
|
|
403
|
+
* 그래서 한 표준에 방언이 둘이 됐다. 문을 낸다.
|
|
404
|
+
*/
|
|
405
|
+
export declare function classUri(namespace: string, classId: string | number): string | undefined;
|
|
388
406
|
export declare function bizTransactionUri(namespace: string, transId: string | number): string | undefined;
|
|
389
407
|
/** 모든 빌더가 공통으로 받는 표준 헤더 옵션(선택) — 방출부가 필요할 때 채운다. */
|
|
390
408
|
export interface EpcisHeaderOptions {
|
package/dist/epcis.js
CHANGED
|
@@ -263,6 +263,26 @@ export function gdtiUri(companyPrefix, docType, serial) {
|
|
|
263
263
|
export function objectUri(namespace, objId) {
|
|
264
264
|
return underNamespace(namespace, 'obj', objId);
|
|
265
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* **선언된 이름공간 아래의 클래스 식별자** — SGTIN·LGTIN 을 쓰지 않는 길.
|
|
268
|
+
*
|
|
269
|
+
* 품목 등급·로트 등급처럼 「낱개가 아니라 종류」를 가리키는 자리다. 개체 쪽(`objectUri`)과 같은
|
|
270
|
+
* 사정으로 필요하다: GTIN 을 조립하려면 회사 프리픽스가 있어야 하고, 없는 현장이 그 길로 못 간다.
|
|
271
|
+
*
|
|
272
|
+
* · CBV 2.0 §8.3.4 `http(s)://[Subdomain.]Domain/⁎⁎/class/ClassID`
|
|
273
|
+
* · CBV 2.0 §8.3.3 `urn:URNNamespace:⁎⁎:class:ClassID`
|
|
274
|
+
*
|
|
275
|
+
* ── 왜 계약에 두나 (2026-09-04, plant 레인이 올림) ─────────────────────────
|
|
276
|
+
* 체프 커넥터가 이 URI 를 **직접 조립하고 있었다** — `productClassId` · `lotClassId` 라는 자기
|
|
277
|
+
* 이름으로. 그중 `bizTransactionId` 는 이 파일의 `bizTransactionUri` 와 **같은 것을 다르게**
|
|
278
|
+
* 만들고 있었다. `underNamespace` 는 처음부터 `'class'` 표지를 알고 있었고(그 함수 주석에
|
|
279
|
+
* §8.3.3·§8.3.4 까지 적혀 있다) **부를 문만 없었다.**
|
|
280
|
+
*
|
|
281
|
+
* 그래서 한 표준에 방언이 둘이 됐다. 문을 낸다.
|
|
282
|
+
*/
|
|
283
|
+
export function classUri(namespace, classId) {
|
|
284
|
+
return underNamespace(namespace, 'class', classId);
|
|
285
|
+
}
|
|
266
286
|
export function bizTransactionUri(namespace, transId) {
|
|
267
287
|
return underNamespace(namespace, 'bt', transId);
|
|
268
288
|
}
|
|
@@ -271,15 +291,44 @@ export function bizTransactionUri(namespace, transId) {
|
|
|
271
291
|
*
|
|
272
292
|
* `obj`(개체 §8.2.3·§8.2.4) · `class`(클래스 §8.3.3·§8.3.4) · `bt`(거래문서 §8.5.4·§8.5.5) 가 같은
|
|
273
293
|
* 모양이다. 규칙을 세 곳에 적으면 한 곳만 고쳐지는 날이 온다.
|
|
294
|
+
*
|
|
295
|
+
* ── 구분자를 계약이 부호화한다 (2026-09-04) ────────────────────────────────
|
|
296
|
+
* 전에는 구분자가 들면 **아무것도 만들지 않았다.** 그러면 부르는 쪽이 자기 손으로 부호화해서
|
|
297
|
+
* 넘기게 되고 — 실제로 그랬다 — 부르는 쪽마다 부호화가 달라진다. 방금 `classUri` 가 없어서
|
|
298
|
+
* 방언이 생긴 것과 **같은 부류**다. 표지 규칙만 계약이 갖고 부호화는 소비처에 남기면 반쪽이다.
|
|
299
|
+
*
|
|
300
|
+
* 그래서 **원문을 받아 계약이 부호화한다.** 부르는 쪽은 미리 부호화하지 않는다 — 하면 두 번
|
|
301
|
+
* 부호화되어(`A%2FB` → `A%252FB`) 다른 개체가 된다.
|
|
302
|
+
*
|
|
303
|
+
* ── 저장된 식별자를 건드리지 않는다 ────────────────────────────────────────
|
|
304
|
+
* 이 함수가 내는 값은 **저널에 남는 정체성**이다(인티그레이션 레인 실측: 47만 건 넘음). 한 글자만
|
|
305
|
+
* 달라도 이전 사실과 새 사실이 다른 개체가 된다. 그래서 **구조를 깨는 글자만** 부호화한다.
|
|
306
|
+
*
|
|
307
|
+
* 부호화한다 % / : ? # 공백 URL 경로 성분이나 URN 성분 경계를 깬다(`%` 는 부호화의
|
|
308
|
+
* 부호이므로 먼저 해야 뜻이 하나로 정해진다)
|
|
309
|
+
* 그대로 둔다 그 밖의 모든 글자 영숫자 · `-` · `.` · `_` · `~` 를 포함해 전부
|
|
310
|
+
*
|
|
311
|
+
* 그래서 **위 여섯 글자가 없는 값은 바이트까지 그대로다** — 지금 저장된 값은 그 집합에 들었나만
|
|
312
|
+
* 확인하면 된다(그 확인은 값을 가진 쪽이 한다). 전에 통과하던 값 중 구분자가 든 것은 없었으므로
|
|
313
|
+
* (구분자가 들면 만들지 않았다) 이 변경으로 **바뀔 수 있는 것은 공백·`%`·`?`·`#` 이 든 값뿐**이다.
|
|
274
314
|
*/
|
|
315
|
+
const STRUCTURE_BREAKING = /[%/:?# ]/g;
|
|
316
|
+
const PERCENT_ENCODED = {
|
|
317
|
+
'%': '%25',
|
|
318
|
+
'/': '%2F',
|
|
319
|
+
':': '%3A',
|
|
320
|
+
'?': '%3F',
|
|
321
|
+
'#': '%23',
|
|
322
|
+
' ': '%20'
|
|
323
|
+
};
|
|
275
324
|
function underNamespace(namespace, marker, id) {
|
|
276
325
|
const ns = namespace?.trim();
|
|
277
326
|
if (!ns)
|
|
278
327
|
return undefined;
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
if (!v || v.includes('/') || v.includes(':'))
|
|
328
|
+
const raw = String(id);
|
|
329
|
+
if (!raw)
|
|
282
330
|
return undefined;
|
|
331
|
+
const v = raw.replace(STRUCTURE_BREAKING, c => PERCENT_ENCODED[c]);
|
|
283
332
|
if (/^https?:\/\/[^/\s]+/.test(ns))
|
|
284
333
|
return `${ns.replace(/\/+$/, '')}/${marker}/${v}`;
|
|
285
334
|
/*
|
|
@@ -447,7 +496,30 @@ const CBV_URN_CLASS = /^urn:[^:\s]+:(?:[^:\s]+:)*class:[^:\s]+$/;
|
|
|
447
496
|
* 합계만 본다 — 프리픽스가 몇 자리인지는 GS1 이 회사마다 다르게 배정하므로 우리가 알 수 없다.
|
|
448
497
|
* 합계는 키 종류가 정한다: SGTIN 13(GTIN-14 의 표시자+13자리) · SSCC 17 · GRAI 12 · GDTI 12.
|
|
449
498
|
*/
|
|
450
|
-
|
|
499
|
+
/*
|
|
500
|
+
* 키마다 「숫자 마디의 자릿수 합」 — 이 표에 없는 키는 아래 함수가 **아무 말도 하지 않는다.**
|
|
501
|
+
*
|
|
502
|
+
* ── lgtin 이 빠져 있었다 (2026-09-04, plant 레인 실측) ──────────────────────
|
|
503
|
+
* `urn:epc:class:lgtin:880.01.LOT-A` 는 합이 5 인데 그냥 지났고, 같은 값을 sgtin 으로 적으면
|
|
504
|
+
* 거절됐다. **하필 로트로 관리하는 자재가 쓰는 종류다** — 원자재·화학·식품이 다 여기다.
|
|
505
|
+
*
|
|
506
|
+
* 규칙은 이미 이 파일의 `lgtinClass` 주석에 적혀 있었다("두 숫자 마디의 자릿수 합은 13", EPC TDS
|
|
507
|
+
* 2.1.0 §6.4.1). 규칙을 알고, 검사도 있고, **표에 키만 없었다.**
|
|
508
|
+
*
|
|
509
|
+
* 셋째 마디(로트)는 숫자가 아니다 — 아래 함수가 앞의 두 마디만 세므로 그대로 맞다.
|
|
510
|
+
*
|
|
511
|
+
* ── sgln 도 같이 없었다 ────────────────────────────────────────────────────
|
|
512
|
+
* 로트를 찾다가 봤다. GLN 은 검사숫자를 뺀 두 마디 합이 12 라 `grai`·`gdti` 와 같은 규칙인데
|
|
513
|
+
* 표에 없었다.
|
|
514
|
+
*
|
|
515
|
+
* 값이 흐르고 있는지는 재봤다: **아직 안 흐른다.** 접수면에 자리가 있고(`RefLocation.gs1Id`)
|
|
516
|
+
* 매핑도 그것을 옮기는데, plant 마스터의 자리 표에는 그 컬럼 자체가 없다(`properties` 안에도
|
|
517
|
+
* 없다 — 0건). 그러니 이 키를 더해서 **지금 거절되는 값은 없다.** 오는 날에 맞아 있으려고 둔다.
|
|
518
|
+
*
|
|
519
|
+
* `giai`(개별 자산)는 일부러 없다 — 자산 참조 길이가 가변이어서 **합이 고정되지 않는다**(전체
|
|
520
|
+
* 30자리 이내라는 상한만 있다). 자릿수로 판정할 수 없는 키를 표에 넣으면 맞는 값을 거절한다.
|
|
521
|
+
*/
|
|
522
|
+
const GS1_KEY_DIGITS = { sgtin: 13, lgtin: 13, sscc: 17, grai: 12, gdti: 12, sgln: 12 };
|
|
451
523
|
/**
|
|
452
524
|
* 그 GS1 키의 자리 수가 맞나 — 어긋나면 왜인지 말한다.
|
|
453
525
|
*
|
|
@@ -19,6 +19,34 @@ export interface OperationalIngestOptions {
|
|
|
19
19
|
/** 레코드에 시각이 없을 때 쓸 값 — 주지 않으면 그 레코드를 거부한다. */
|
|
20
20
|
defaultEventTime?: string;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* **객체 안쪽의 모양** — `object`·`object[]` 자리가 계약의 타입을 실제로 보게 한다.
|
|
24
|
+
*
|
|
25
|
+
* ── 왜 필요한가 (2026-09-04 실측, 인티그레이션 레인이 찾음) ────────────────
|
|
26
|
+
* `materialActual: 'object[]'` 은 **객체 배열이면 지나갔다.** 그래서 틀린 이름이 저널에 앉았다.
|
|
27
|
+
*
|
|
28
|
+
* 계약의 이름 definitionId
|
|
29
|
+
* 온 것 materialDefinitionId
|
|
30
|
+
*
|
|
31
|
+
* 적히나 예 — 저널에 그 줄이 그대로 있다
|
|
32
|
+
* 읽히나 아니오 — 계약의 타입으로 읽으면 undefined 다
|
|
33
|
+
*
|
|
34
|
+
* 거절도 경고도 없었다. **같은 계약의 ERP 경로는 이것을 잡는다**(§`erp.ts` 의 자재 줄 검사).
|
|
35
|
+
* 한 계약 안에 검사가 두 벌이고 한쪽만 봤다.
|
|
36
|
+
*
|
|
37
|
+
* 이름을 맞춰 달라고 원본에 청하는 것으로는 다음에 또 조용해진다 — 다음 원본이 `use: 'out'` 같은
|
|
38
|
+
* 것을 보내면 같은 일이 난다(실제로 그런 시드가 있었다: 계약에 `out` 이라는 낱말이 없다).
|
|
39
|
+
*/
|
|
40
|
+
export interface ObjectShape {
|
|
41
|
+
/** 반드시 있어야 하는 안쪽 필드 — 비어 있으면 그 줄을 거절한다. */
|
|
42
|
+
required?: readonly string[];
|
|
43
|
+
/** 안쪽 필드의 형 — 선언한 것만 본다. */
|
|
44
|
+
fields?: Readonly<Record<string, 'string' | 'number' | 'boolean'>>;
|
|
45
|
+
/** 낱말이 정해진 안쪽 필드 — 계약에 없는 낱말을 거절한다. */
|
|
46
|
+
enums?: Readonly<Record<string, readonly string[]>>;
|
|
47
|
+
/** 0 보다 커야 하는 수 — 「모른다」는 **비워서** 말한다(0 으로 말하지 않는다). */
|
|
48
|
+
positive?: readonly string[];
|
|
49
|
+
}
|
|
22
50
|
/**
|
|
23
51
|
* 이 레코드가 어느 운영 사실인가 — **라우팅 판정을 한 곳에 둔다**(소비처가 각자 짐작하지 않게).
|
|
24
52
|
*
|
|
@@ -34,7 +34,59 @@
|
|
|
34
34
|
* 모르는 필드는 **조용히 버리지 않고 거부한다.** `taskID` 처럼 한 글자 틀린 이름은 통과시키면 영원히
|
|
35
35
|
* 보이지 않는 손실이 된다(이 문에는 아직 옛 발신자가 없어 호환 부담도 없다).
|
|
36
36
|
*/
|
|
37
|
-
import { OP_EVENT, DISPOSITION_DECISION } from "./contract.js";
|
|
37
|
+
import { OP_EVENT, DISPOSITION_DECISION, MATERIAL_ACTUAL_USE } from "./contract.js";
|
|
38
|
+
/**
|
|
39
|
+
* 객체 한 줄이 선언된 안쪽 모양을 지키나 — **선언하지 않은 자리는 보지 않는다.**
|
|
40
|
+
*
|
|
41
|
+
* 순수하다. 그래서 「어떤 줄이 거절되나」를 값만 바꿔 가며 잴 수 있다.
|
|
42
|
+
*
|
|
43
|
+
* ── 「모른다」를 0 으로 말하지 않는다 ───────────────────────────────────────
|
|
44
|
+
* `positive` 에 든 수는 0 보다 커야 한다. 라벨에 수량이 없는 것이 정상인 현장이 있는데(한 통·한
|
|
45
|
+
* 팔레트), 그때 **비워서** 말한다 — 0 으로 실으면 받는 쪽에서 「0개」와 「모른다」가 같아지고,
|
|
46
|
+
* 소요량 집계가 조용히 틀린다.
|
|
47
|
+
*
|
|
48
|
+
* 없는 필드는 `required` 가 아니면 그냥 없는 것이다. 이 함수가 채워 주지 않는다.
|
|
49
|
+
*/
|
|
50
|
+
function shapeErrors(where, row, shape) {
|
|
51
|
+
if (!shape)
|
|
52
|
+
return [];
|
|
53
|
+
const errors = [];
|
|
54
|
+
for (const f of shape.required ?? []) {
|
|
55
|
+
const v = row[f];
|
|
56
|
+
const empty = v === undefined || v === null || (typeof v === 'string' && !v.trim());
|
|
57
|
+
if (empty)
|
|
58
|
+
errors.push(`${where}.${f} 가 없다 — 계약이 요구하는 이름이다`);
|
|
59
|
+
}
|
|
60
|
+
for (const [f, t] of Object.entries(shape.fields ?? {})) {
|
|
61
|
+
const v = row[f];
|
|
62
|
+
if (v === undefined || v === null)
|
|
63
|
+
continue;
|
|
64
|
+
if (t === 'number' && (typeof v !== 'number' || !Number.isFinite(v))) {
|
|
65
|
+
errors.push(`${where}.${f} 가 수가 아니다: ${JSON.stringify(v)}`);
|
|
66
|
+
}
|
|
67
|
+
else if (t !== 'number' && typeof v !== t) {
|
|
68
|
+
errors.push(`${where}.${f} 가 ${t} 가 아니다: ${JSON.stringify(v)}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
for (const [f, allowed] of Object.entries(shape.enums ?? {})) {
|
|
72
|
+
const v = row[f];
|
|
73
|
+
if (v === undefined || v === null)
|
|
74
|
+
continue;
|
|
75
|
+
if (!allowed.includes(String(v))) {
|
|
76
|
+
errors.push(`${where}.${f} 에 계약에 없는 낱말이 왔다: ${JSON.stringify(v)} (${allowed.join(' · ')})`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
for (const f of shape.positive ?? []) {
|
|
80
|
+
const v = row[f];
|
|
81
|
+
/* 없는 것은 「모른다」다 — 여기서 막지 않는다. 위 `required` 가 필요하면 거기서 막는다. */
|
|
82
|
+
if (v === undefined || v === null)
|
|
83
|
+
continue;
|
|
84
|
+
if (typeof v !== 'number' || !(v > 0)) {
|
|
85
|
+
errors.push(`${where}.${f} 가 0 이하다: ${JSON.stringify(v)} — 모르는 수량은 0 이 아니라 비워서 말한다`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return errors;
|
|
89
|
+
}
|
|
38
90
|
/**
|
|
39
91
|
* 닫아 둔 낱말과 그 이유.
|
|
40
92
|
* · 작업 상태 — 성과 폴드가 `completed`·`in-progress` 로 갈린다(`kpi-fold`).
|
|
@@ -72,6 +124,27 @@ const SPECS = {
|
|
|
72
124
|
startedAtSimMs: 'number', outcome: 'string', priority: 'number', startTime: 'string', endTime: 'string',
|
|
73
125
|
materialActual: 'object[]', recordTime: 'string'
|
|
74
126
|
},
|
|
127
|
+
/*
|
|
128
|
+
* **자재 줄의 안쪽** — 계약의 `MaterialActual` 그대로다.
|
|
129
|
+
*
|
|
130
|
+
* `definitionId` 를 요구하는 이유: 이 이름이 없으면 그 줄이 무엇을 가리키는지 아무도 모른다.
|
|
131
|
+
* 실제로 `materialDefinitionId` 로 온 줄이 저널에 앉았고, 계약의 타입으로 읽으면 비어 있었다.
|
|
132
|
+
*
|
|
133
|
+
* `quantity` 를 `positive` 에 두는 이유: 「모른다」는 **비워서** 말한다. 0 으로 실으면 받는
|
|
134
|
+
* 쪽에서 「0개」와 구별되지 않고, 소요량 집계가 조용히 틀린다.
|
|
135
|
+
*
|
|
136
|
+
* `uom` 은 요구하지 않는다 — 계약에서 선택이다(§`MaterialActual`). ERP 경로는 요구하는데
|
|
137
|
+
* 그것은 **그 경로의 규칙**이다(정산에 단위 없는 수량을 쓸 수 없다). 커널이 그 규칙을 통째로
|
|
138
|
+
* 물려받으면 단위를 모르는 현장의 사실이 통째로 거절된다.
|
|
139
|
+
*/
|
|
140
|
+
shapes: {
|
|
141
|
+
materialActual: {
|
|
142
|
+
required: ['definitionId', 'use'],
|
|
143
|
+
fields: { definitionId: 'string', lotId: 'string', quantity: 'number', uom: 'string' },
|
|
144
|
+
enums: { use: MATERIAL_ACTUAL_USE },
|
|
145
|
+
positive: ['quantity']
|
|
146
|
+
}
|
|
147
|
+
},
|
|
75
148
|
enums: {
|
|
76
149
|
status: TASK_STATUS,
|
|
77
150
|
intent: ['transport', 'process', 'dwell'],
|
|
@@ -428,6 +501,7 @@ export function ingestOperationalRecords(records, opts) {
|
|
|
428
501
|
errors.push(`${kind}.${name} 가 객체가 아니다: ${JSON.stringify(v)}`);
|
|
429
502
|
break;
|
|
430
503
|
}
|
|
504
|
+
errors.push(...shapeErrors(`${kind}.${name}`, v, spec.shapes?.[name]));
|
|
431
505
|
data[name] = { ...v };
|
|
432
506
|
break;
|
|
433
507
|
}
|
|
@@ -436,6 +510,7 @@ export function ingestOperationalRecords(records, opts) {
|
|
|
436
510
|
errors.push(`${kind}.${name} 가 객체 배열이 아니다: ${JSON.stringify(v)}`);
|
|
437
511
|
break;
|
|
438
512
|
}
|
|
513
|
+
v.forEach((x, i) => errors.push(...shapeErrors(`${kind}.${name}[${i}]`, x, spec.shapes?.[name])));
|
|
439
514
|
data[name] = v.map(x => ({ ...x }));
|
|
440
515
|
break;
|
|
441
516
|
}
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -50,6 +50,7 @@ __export(index_exports, {
|
|
|
50
50
|
GUARD_PRAGMA: () => GUARD_PRAGMA,
|
|
51
51
|
ILMD_ATTR: () => ILMD_ATTR,
|
|
52
52
|
LOCATION_SATURATION_NEAR: () => LOCATION_SATURATION_NEAR,
|
|
53
|
+
MATERIAL_ACTUAL_USE: () => MATERIAL_ACTUAL_USE,
|
|
53
54
|
MATERIAL_PROPERTY: () => MATERIAL_PROPERTY,
|
|
54
55
|
MES_BIZSTEP: () => MES_BIZSTEP,
|
|
55
56
|
MES_COMMANDS: () => MES_COMMANDS,
|
|
@@ -95,6 +96,7 @@ __export(index_exports, {
|
|
|
95
96
|
checkSequenceRun: () => checkSequenceRun,
|
|
96
97
|
classClosure: () => classClosure,
|
|
97
98
|
classIdentifierViolation: () => classIdentifierViolation,
|
|
99
|
+
classUri: () => classUri,
|
|
98
100
|
commandFromSpec: () => commandFromSpec,
|
|
99
101
|
commandSpecGaps: () => commandSpecGaps,
|
|
100
102
|
commandTypeOf: () => commandTypeOf,
|
|
@@ -620,8 +622,8 @@ function dueStatusOf(x, nowIso) {
|
|
|
620
622
|
if (!Number.isFinite(due) || !Number.isFinite(now)) return void 0;
|
|
621
623
|
return now > due ? "late" : "on-time";
|
|
622
624
|
}
|
|
623
|
-
function subLotIdOf(
|
|
624
|
-
return `${
|
|
625
|
+
function subLotIdOf(classUri2, location) {
|
|
626
|
+
return `${classUri2}@${location}`;
|
|
625
627
|
}
|
|
626
628
|
function itemKeyOf(item) {
|
|
627
629
|
return item.subLotId ?? item.epc;
|
|
@@ -1032,6 +1034,7 @@ function isFailureStatus(value) {
|
|
|
1032
1034
|
function isPlannedStopStatus(value) {
|
|
1033
1035
|
return normalizeEquipmentStatus(value) === "planned-stop";
|
|
1034
1036
|
}
|
|
1037
|
+
var MATERIAL_ACTUAL_USE = ["consumed", "produced"];
|
|
1035
1038
|
var CMD = {
|
|
1036
1039
|
orderHold: "order.hold",
|
|
1037
1040
|
orderResume: "order.resume",
|
|
@@ -2791,14 +2794,27 @@ function gdtiUri(companyPrefix, docType, serial) {
|
|
|
2791
2794
|
function objectUri(namespace, objId) {
|
|
2792
2795
|
return underNamespace(namespace, "obj", objId);
|
|
2793
2796
|
}
|
|
2797
|
+
function classUri(namespace, classId) {
|
|
2798
|
+
return underNamespace(namespace, "class", classId);
|
|
2799
|
+
}
|
|
2794
2800
|
function bizTransactionUri(namespace, transId) {
|
|
2795
2801
|
return underNamespace(namespace, "bt", transId);
|
|
2796
2802
|
}
|
|
2803
|
+
var STRUCTURE_BREAKING = /[%/:?# ]/g;
|
|
2804
|
+
var PERCENT_ENCODED = {
|
|
2805
|
+
"%": "%25",
|
|
2806
|
+
"/": "%2F",
|
|
2807
|
+
":": "%3A",
|
|
2808
|
+
"?": "%3F",
|
|
2809
|
+
"#": "%23",
|
|
2810
|
+
" ": "%20"
|
|
2811
|
+
};
|
|
2797
2812
|
function underNamespace(namespace, marker, id) {
|
|
2798
2813
|
const ns = namespace?.trim();
|
|
2799
2814
|
if (!ns) return void 0;
|
|
2800
|
-
const
|
|
2801
|
-
if (!
|
|
2815
|
+
const raw = String(id);
|
|
2816
|
+
if (!raw) return void 0;
|
|
2817
|
+
const v = raw.replace(STRUCTURE_BREAKING, (c) => PERCENT_ENCODED[c]);
|
|
2802
2818
|
if (/^https?:\/\/[^/\s]+/.test(ns)) return `${ns.replace(/\/+$/, "")}/${marker}/${v}`;
|
|
2803
2819
|
if (/^urn:epc(global)?:/.test(ns)) return void 0;
|
|
2804
2820
|
if (/^urn:[^:\s]+/.test(ns)) return `${ns.replace(/:+$/, "")}:${marker}:${v}`;
|
|
@@ -2877,7 +2893,7 @@ var EPC_CLASS_PREFIXES = ["urn:epc:idpat:", "urn:epc:class:"];
|
|
|
2877
2893
|
var DL_CANONICAL = "https://id.gs1.org/";
|
|
2878
2894
|
var CBV_URL_CLASS = /^https?:\/\/[^/\s]+\/(?:[^/\s]+\/)*class\/[^/\s]+$/;
|
|
2879
2895
|
var CBV_URN_CLASS = /^urn:[^:\s]+:(?:[^:\s]+:)*class:[^:\s]+$/;
|
|
2880
|
-
var GS1_KEY_DIGITS = { sgtin: 13, sscc: 17, grai: 12, gdti: 12 };
|
|
2896
|
+
var GS1_KEY_DIGITS = { sgtin: 13, lgtin: 13, sscc: 17, grai: 12, gdti: 12, sgln: 12 };
|
|
2881
2897
|
function gs1KeyDigitViolation(uri) {
|
|
2882
2898
|
if (!uri) return void 0;
|
|
2883
2899
|
const m = /^urn:epc:(?:id|idpat|class):([a-z]+):([^:]+)$/.exec(uri);
|
|
@@ -3257,6 +3273,39 @@ function readEpochMs(raw) {
|
|
|
3257
3273
|
}
|
|
3258
3274
|
|
|
3259
3275
|
// src/operational-ingest.ts
|
|
3276
|
+
function shapeErrors(where, row, shape) {
|
|
3277
|
+
if (!shape) return [];
|
|
3278
|
+
const errors = [];
|
|
3279
|
+
for (const f of shape.required ?? []) {
|
|
3280
|
+
const v = row[f];
|
|
3281
|
+
const empty = v === void 0 || v === null || typeof v === "string" && !v.trim();
|
|
3282
|
+
if (empty) errors.push(`${where}.${f} \uAC00 \uC5C6\uB2E4 \u2014 \uACC4\uC57D\uC774 \uC694\uAD6C\uD558\uB294 \uC774\uB984\uC774\uB2E4`);
|
|
3283
|
+
}
|
|
3284
|
+
for (const [f, t] of Object.entries(shape.fields ?? {})) {
|
|
3285
|
+
const v = row[f];
|
|
3286
|
+
if (v === void 0 || v === null) continue;
|
|
3287
|
+
if (t === "number" && (typeof v !== "number" || !Number.isFinite(v))) {
|
|
3288
|
+
errors.push(`${where}.${f} \uAC00 \uC218\uAC00 \uC544\uB2C8\uB2E4: ${JSON.stringify(v)}`);
|
|
3289
|
+
} else if (t !== "number" && typeof v !== t) {
|
|
3290
|
+
errors.push(`${where}.${f} \uAC00 ${t} \uAC00 \uC544\uB2C8\uB2E4: ${JSON.stringify(v)}`);
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
for (const [f, allowed] of Object.entries(shape.enums ?? {})) {
|
|
3294
|
+
const v = row[f];
|
|
3295
|
+
if (v === void 0 || v === null) continue;
|
|
3296
|
+
if (!allowed.includes(String(v))) {
|
|
3297
|
+
errors.push(`${where}.${f} \uC5D0 \uACC4\uC57D\uC5D0 \uC5C6\uB294 \uB0B1\uB9D0\uC774 \uC654\uB2E4: ${JSON.stringify(v)} (${allowed.join(" \xB7 ")})`);
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
for (const f of shape.positive ?? []) {
|
|
3301
|
+
const v = row[f];
|
|
3302
|
+
if (v === void 0 || v === null) continue;
|
|
3303
|
+
if (typeof v !== "number" || !(v > 0)) {
|
|
3304
|
+
errors.push(`${where}.${f} \uAC00 0 \uC774\uD558\uB2E4: ${JSON.stringify(v)} \u2014 \uBAA8\uB974\uB294 \uC218\uB7C9\uC740 0 \uC774 \uC544\uB2C8\uB77C \uBE44\uC6CC\uC11C \uB9D0\uD55C\uB2E4`);
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
return errors;
|
|
3308
|
+
}
|
|
3260
3309
|
var TASK_STATUS = ["created", "assigned", "in-progress", "completed"];
|
|
3261
3310
|
var EQUIPMENT_STATUS2 = ["idle", "busy", "down", "setup", "planned-stop"];
|
|
3262
3311
|
var PERSON_STATUS = ["idle", "busy"];
|
|
@@ -3293,6 +3342,27 @@ var SPECS = {
|
|
|
3293
3342
|
materialActual: "object[]",
|
|
3294
3343
|
recordTime: "string"
|
|
3295
3344
|
},
|
|
3345
|
+
/*
|
|
3346
|
+
* **자재 줄의 안쪽** — 계약의 `MaterialActual` 그대로다.
|
|
3347
|
+
*
|
|
3348
|
+
* `definitionId` 를 요구하는 이유: 이 이름이 없으면 그 줄이 무엇을 가리키는지 아무도 모른다.
|
|
3349
|
+
* 실제로 `materialDefinitionId` 로 온 줄이 저널에 앉았고, 계약의 타입으로 읽으면 비어 있었다.
|
|
3350
|
+
*
|
|
3351
|
+
* `quantity` 를 `positive` 에 두는 이유: 「모른다」는 **비워서** 말한다. 0 으로 실으면 받는
|
|
3352
|
+
* 쪽에서 「0개」와 구별되지 않고, 소요량 집계가 조용히 틀린다.
|
|
3353
|
+
*
|
|
3354
|
+
* `uom` 은 요구하지 않는다 — 계약에서 선택이다(§`MaterialActual`). ERP 경로는 요구하는데
|
|
3355
|
+
* 그것은 **그 경로의 규칙**이다(정산에 단위 없는 수량을 쓸 수 없다). 커널이 그 규칙을 통째로
|
|
3356
|
+
* 물려받으면 단위를 모르는 현장의 사실이 통째로 거절된다.
|
|
3357
|
+
*/
|
|
3358
|
+
shapes: {
|
|
3359
|
+
materialActual: {
|
|
3360
|
+
required: ["definitionId", "use"],
|
|
3361
|
+
fields: { definitionId: "string", lotId: "string", quantity: "number", uom: "string" },
|
|
3362
|
+
enums: { use: MATERIAL_ACTUAL_USE },
|
|
3363
|
+
positive: ["quantity"]
|
|
3364
|
+
}
|
|
3365
|
+
},
|
|
3296
3366
|
enums: {
|
|
3297
3367
|
status: TASK_STATUS,
|
|
3298
3368
|
intent: ["transport", "process", "dwell"],
|
|
@@ -3675,6 +3745,7 @@ function ingestOperationalRecords(records, opts) {
|
|
|
3675
3745
|
errors.push(`${kind}.${name} \uAC00 \uAC1D\uCCB4\uAC00 \uC544\uB2C8\uB2E4: ${JSON.stringify(v)}`);
|
|
3676
3746
|
break;
|
|
3677
3747
|
}
|
|
3748
|
+
errors.push(...shapeErrors(`${kind}.${name}`, v, spec.shapes?.[name]));
|
|
3678
3749
|
data[name] = { ...v };
|
|
3679
3750
|
break;
|
|
3680
3751
|
}
|
|
@@ -3683,6 +3754,9 @@ function ingestOperationalRecords(records, opts) {
|
|
|
3683
3754
|
errors.push(`${kind}.${name} \uAC00 \uAC1D\uCCB4 \uBC30\uC5F4\uC774 \uC544\uB2C8\uB2E4: ${JSON.stringify(v)}`);
|
|
3684
3755
|
break;
|
|
3685
3756
|
}
|
|
3757
|
+
v.forEach(
|
|
3758
|
+
(x, i) => errors.push(...shapeErrors(`${kind}.${name}[${i}]`, x, spec.shapes?.[name]))
|
|
3759
|
+
);
|
|
3686
3760
|
data[name] = v.map((x) => ({ ...x }));
|
|
3687
3761
|
break;
|
|
3688
3762
|
}
|
|
@@ -4233,6 +4307,7 @@ function commandSpecGaps(specs, command) {
|
|
|
4233
4307
|
GUARD_PRAGMA,
|
|
4234
4308
|
ILMD_ATTR,
|
|
4235
4309
|
LOCATION_SATURATION_NEAR,
|
|
4310
|
+
MATERIAL_ACTUAL_USE,
|
|
4236
4311
|
MATERIAL_PROPERTY,
|
|
4237
4312
|
MES_BIZSTEP,
|
|
4238
4313
|
MES_COMMANDS,
|
|
@@ -4278,6 +4353,7 @@ function commandSpecGaps(specs, command) {
|
|
|
4278
4353
|
checkSequenceRun,
|
|
4279
4354
|
classClosure,
|
|
4280
4355
|
classIdentifierViolation,
|
|
4356
|
+
classUri,
|
|
4281
4357
|
commandFromSpec,
|
|
4282
4358
|
commandSpecGaps,
|
|
4283
4359
|
commandTypeOf,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@operato/ops-contract",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.4",
|
|
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",
|