@operato/ops-contract 0.9.2 → 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 +130 -7
- package/dist/contract.js +77 -0
- package/dist/epcis.d.ts +18 -0
- package/dist/epcis.js +76 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/oee.d.ts +8 -1
- package/dist/oee.js +2 -2
- package/dist/operational-ingest.d.ts +28 -0
- package/dist/operational-ingest.js +79 -4
- package/dist/rated-usage.d.ts +59 -0
- package/dist/rated-usage.js +60 -0
- package/dist/vocabulary.d.ts +1 -1
- package/dist/vocabulary.js +1 -1
- package/dist-cjs/index.cjs +136 -24
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type RatedUsage, type UsedUsage } from './rated-usage.ts';
|
|
1
2
|
export type ISOTime = string;
|
|
2
3
|
export interface CanonicalEnvelope<T = unknown> {
|
|
3
4
|
eventId: string;
|
|
@@ -476,6 +477,8 @@ export type CapabilityReason =
|
|
|
476
477
|
| 'resting'
|
|
477
478
|
/** 요구된 시험의 결과가 만료·불합격 — 자격이 성립하지 않는다(§TestResult). */
|
|
478
479
|
| 'test-expired'
|
|
480
|
+
/** 정격 사용량을 다 썼다 — 교체할 일이다(§`RatedUsage`). 고장(`down`)과 다르다: 고쳐서 쓰지 않는다. */
|
|
481
|
+
| 'worn-out'
|
|
479
482
|
/** 지금 다른 일을 하고 있다 — 능력은 있고 여유가 없다. */
|
|
480
483
|
| 'working'
|
|
481
484
|
/** 쓸 수 있다. */
|
|
@@ -748,6 +751,41 @@ export interface ResourceClassDef extends EffectivePeriod {
|
|
|
748
751
|
testSpecificationIds?: TestSpecificationRefs;
|
|
749
752
|
}
|
|
750
753
|
export declare function classClosure(directIds: readonly string[] | undefined, defs: readonly ResourceClassDef[] | undefined, at?: ISOTime): Set<string>;
|
|
754
|
+
/**
|
|
755
|
+
* 설비의 **등급 소속** — 선언한 등급들과 `kind` 를 합친 것.
|
|
756
|
+
*
|
|
757
|
+
* ── 왜 설비만 함수가 있나 ────────────────────────────────────────────────────
|
|
758
|
+
* 사람과 자산은 소속이 `personnelClassIds`·`assetClassIds` 하나뿐이라 부르는 쪽이 그 필드를 그대로
|
|
759
|
+
* 넘긴다. 설비는 소속이 **두 곳에서 온다** — 선언한 `equipmentClassIds` 와, 예전부터 소속 노릇을 해 온
|
|
760
|
+
* `kind`. 그 합침을 부르는 쪽마다 손으로 쓰면 한 곳이 빠진 날 그 설비는 **거기서만 등급을 잃는다.**
|
|
761
|
+
*
|
|
762
|
+
* 실제로 그 자리가 여섯이다: 스냅샷·가용 개수·배정 고르기·재수화·op 이벤트·관측 축소. 여섯이 같은
|
|
763
|
+
* 답을 내야 하므로 규칙이 한 자리에 있다(`requiredTestsFor` 가 계약에 있는 것과 같은 이유).
|
|
764
|
+
*
|
|
765
|
+
* ── `kind` 를 왜 소속으로 세나 (2026-09-03, 재고 결정) ──────────────────────
|
|
766
|
+
* 처음에는 설비 소속이 `kind` **하나**였다. 그것을 복수로 넓히면서 `kind` 를 뺄지 재 봤다.
|
|
767
|
+
*
|
|
768
|
+
* ```
|
|
769
|
+
* 저장된 트윈 32개 · kind 19종(cutter welder painter assembler hostler forklift meter pv-array …)
|
|
770
|
+
* equipmentClasses 를 선언한 트윈 0
|
|
771
|
+
* 설비를 이름으로 부르는 공정 52 그 이름이 전부 kind (cutter · welder · painter · assembler)
|
|
772
|
+
* ```
|
|
773
|
+
*
|
|
774
|
+
* **`kind` 는 지금 하중을 받고 있다.** 빼면 그 52 공정이 설비를 못 찾고, 요구가 채워지지 않아 라인이
|
|
775
|
+
* 굶는다 — 오류는 안 난다. 그리고 표준에서도 `EquipmentClass` 는 "정해진 목적으로 묶은 설비 무리" 이니
|
|
776
|
+
* 설비의 종류는 그 자체로 등급이다. 그래서 뺄 이유가 없고, **선언한 것에 더한다.**
|
|
777
|
+
*
|
|
778
|
+
* 합치는 것이 위험한 경우는 하나다 — 선언한 등급 id 가 어떤 `kind` 와 우연히 같을 때. 그때는 그 설비가
|
|
779
|
+
* 뜻하지 않은 요구를 만족한다. 다만 등급 id 는 사람이 일부러 짓는 이름이고 `kind` 도 그렇다. 같은
|
|
780
|
+
* 이름을 지었다면 같은 것을 뜻한 것으로 본다.
|
|
781
|
+
*
|
|
782
|
+
* 순서는 **선언한 것이 먼저**다 — 부르는 쪽이 첫 값을 대표 등급으로 쓸 때 종류가 아니라 선언이 대표가
|
|
783
|
+
* 된다. 중복은 접는다.
|
|
784
|
+
*/
|
|
785
|
+
export declare function equipmentClassMembership(e: {
|
|
786
|
+
kind?: string;
|
|
787
|
+
equipmentClassIds?: readonly string[];
|
|
788
|
+
}): string[];
|
|
751
789
|
/**
|
|
752
790
|
* 우선순위 — **ISA-95 `Priority`**(`JobOrderType`·`OperationsRequestType`, 타입은 `PriorityType` =
|
|
753
791
|
* `NumericType` 제한). 즉 표준은 **숫자라는 것만 정하고 방향은 정하지 않는다.**
|
|
@@ -1079,6 +1117,9 @@ export declare function capabilityOf(r: {
|
|
|
1079
1117
|
};
|
|
1080
1118
|
workCalendar?: WorkCalendarEntry[];
|
|
1081
1119
|
testResults?: TestResult[];
|
|
1120
|
+
/** 정격 사용량과 쓴 양 — 소모 공구만 든다(§`RatedUsage`). 없으면 이 축으로 막지 않는다. */
|
|
1121
|
+
ratedUsage?: RatedUsage;
|
|
1122
|
+
usedUsage?: UsedUsage;
|
|
1082
1123
|
} & EffectivePeriod, ctx?: {
|
|
1083
1124
|
at?: ISOTime;
|
|
1084
1125
|
utcOffsetMinutes?: number;
|
|
@@ -1419,7 +1460,7 @@ export interface OeeMetrics {
|
|
|
1419
1460
|
downMs: number;
|
|
1420
1461
|
delayMs: number;
|
|
1421
1462
|
goodCount: number;
|
|
1422
|
-
|
|
1463
|
+
nonconformingCount: number;
|
|
1423
1464
|
}
|
|
1424
1465
|
/**
|
|
1425
1466
|
* OEE 값이 없는 이유.
|
|
@@ -1439,6 +1480,16 @@ export type OeeMissing =
|
|
|
1439
1480
|
export interface EquipmentState extends EffectivePeriod {
|
|
1440
1481
|
id: string;
|
|
1441
1482
|
kind: string;
|
|
1483
|
+
/**
|
|
1484
|
+
* 속한 **등급들** — 표준 `EquipmentClassID`(복수). 사람·자산의 상태가 등급을 싣는 것과 같다.
|
|
1485
|
+
*
|
|
1486
|
+
* 스냅샷과 op 이벤트가 이것을 실어야 **재기동한 미러가 소속을 이어받는다** — 실지 않으면 되살린
|
|
1487
|
+
* 트윈에서 등급이 비고, 등급으로 부르는 요구(`equipmentSpecification`)가 채워지지 않아 라인이 굶는다.
|
|
1488
|
+
* 오류는 나지 않는다.
|
|
1489
|
+
*
|
|
1490
|
+
* 판정은 `equipmentClassMembership` 을 거친다 — 이 선언과 `kind` 를 합치는 규칙은 한 자리에 있다.
|
|
1491
|
+
*/
|
|
1492
|
+
equipmentClassIds?: string[];
|
|
1442
1493
|
/** **지금 어디에 있나.** 운반 작업이 끝나면 도착 자리로 옮겨진다(제자리 작업은 안 움직인다). */
|
|
1443
1494
|
location?: string;
|
|
1444
1495
|
/**
|
|
@@ -1712,6 +1763,18 @@ export interface AssetState extends EffectivePeriod {
|
|
|
1712
1763
|
* 비어 있으면 빈 팔레트다(회수 대상이자 다음 출고의 재료).
|
|
1713
1764
|
*/
|
|
1714
1765
|
carrying?: string;
|
|
1766
|
+
/**
|
|
1767
|
+
* **정격 사용량** — 이만큼 쓰면 교체한다(§`RatedUsage`).
|
|
1768
|
+
*
|
|
1769
|
+
* 자산에 두는 이유: 닳는 것은 개체다. 등급에 두면 「메탈마스크는 5만 번」은 말할 수 있어도
|
|
1770
|
+
* **「지금 걸려 있는 이 한 장이 4만 7천 번째다」**를 말할 수 없고, 교체해야 하는 것은 그 한 장이다.
|
|
1771
|
+
*
|
|
1772
|
+
* 속성(`properties`)에 두지 않는 이유: 이 값은 **막는 데 쓴다.** 속성 배열을 뒤져 읽으면 오타
|
|
1773
|
+
* 하나가 오류 보고 없이 「정격 없음」이 된다(§`ResourceProperty` — 속성은 열림, 효과는 닫힘).
|
|
1774
|
+
*/
|
|
1775
|
+
ratedUsage?: RatedUsage;
|
|
1776
|
+
/** 지금까지 쓴 양 — 정격과 **같은 단위**여야 견준다(§`UsedUsage`). 없으면 0이 아니라 모르는 것이다. */
|
|
1777
|
+
usedUsage?: UsedUsage;
|
|
1715
1778
|
/** 자원 속성 — 표준 `PhysicalAssetProperty`(§ResourceProperty). */
|
|
1716
1779
|
properties?: ResourceProperty[];
|
|
1717
1780
|
/** 적격을 검증한 시험 명세들 — 표준 `PhysicalAsset.TestSpecificationID`(§TestSpecificationRefs). */
|
|
@@ -3058,6 +3121,15 @@ export interface PersonStatusDelta extends EffectivePeriod {
|
|
|
3058
3121
|
export interface AssetStatusDelta extends EffectivePeriod {
|
|
3059
3122
|
assetId: string;
|
|
3060
3123
|
assetClassIds?: string[];
|
|
3124
|
+
/**
|
|
3125
|
+
* 정격과 쓴 양 — **상태에 있으면 델타에도 있어야 한다**(상태⊆이벤트).
|
|
3126
|
+
*
|
|
3127
|
+
* 특히 쓴 양은 시뮬이 작업마다 올리는 값이라 나가지 않으면 미러가 영원히 처음 값을 들고 있고,
|
|
3128
|
+
* 「이 공구가 언제 소진되나」에 답할 수 없다. 정격은 선언이라 잘 안 바뀌지만 함께 낸다 —
|
|
3129
|
+
* 미러가 정격 없이 쓴 양만 받으면 견줄 것이 없다.
|
|
3130
|
+
*/
|
|
3131
|
+
ratedUsage?: RatedUsage;
|
|
3132
|
+
usedUsage?: UsedUsage;
|
|
3061
3133
|
status: string;
|
|
3062
3134
|
location?: string;
|
|
3063
3135
|
taskId?: string;
|
|
@@ -3151,12 +3223,33 @@ export interface EquipmentStatePeriodFact {
|
|
|
3151
3223
|
decidedBy?: string;
|
|
3152
3224
|
recordTime?: ISOTime;
|
|
3153
3225
|
}
|
|
3154
|
-
/**
|
|
3226
|
+
/**
|
|
3227
|
+
* 품질 산출 델타 — recordOutput(양품/부적합) 시 방출. 두 수는 설비 누적값이다.
|
|
3228
|
+
*
|
|
3229
|
+
* ── 이름을 `scrapCount` 에서 바꾼 이유 (2026-09-03) — vocabulary-guard: allow (옛 이름을 글로 남긴다)
|
|
3230
|
+
* 이 수는 **검사에서 떨어진 것 전부**다. 재작업으로 되살아날 것도 든다. 그런데 이름이
|
|
3231
|
+
* `scrapCount` 였고(vocabulary-guard: allow), 처분 어휘의 `scrap` 은 **「폐기」 하나**다(§`DISPOSITION_DECISION`) — 되돌리지 않고 버리는
|
|
3232
|
+
* 것. 같은 낱말이 두 뜻이었다.
|
|
3233
|
+
*
|
|
3234
|
+
* **그리고 ISO 22400-2 의 SQ(scrap quantity)가 아니다.** 표준은 셋을 가른다: GQ(양품) · SQ(폐기,
|
|
3235
|
+
* 재작업할 수 없는 것) · RQ(재작업). 이 수는 `SQ + RQ` 다.
|
|
3236
|
+
*
|
|
3237
|
+
* 비율 계산은 지금도 맞다 — 품질률은 `GQ / (GQ + SQ + RQ)` 이므로 `goodCount / (goodCount +
|
|
3238
|
+
* nonconformingCount)` 가 정확히 그것이다. **틀린 것은 이름뿐이었고**, 그래서 새로 붙이는 쪽이
|
|
3239
|
+
* 이것을 폐기 수로 읽고 폐기 원가를 세거나 재작업 수를 따로 더해 분모를 두 번 셀 위험이 있었다.
|
|
3240
|
+
*
|
|
3241
|
+
* 낱말은 `nonconforming` 으로 정했다 — `reject` 도 검사에서 「떨어뜨려 버린다」는 결정을 뜻해 같은
|
|
3242
|
+
* 모호함을 남긴다. 계약이 이미 `nonconformance.disposition`·`FirstPassInput.nonconforming` 을 쓰고
|
|
3243
|
+
* 있으니 세 번째 낱말을 만들지 않는다. **화면은 「불량 / reject」로 남는다** — 현장이 쓰는 말이고,
|
|
3244
|
+
* 국문이 이미 「불량 수」와 「부적합 처분」을 갈라 쓴다.
|
|
3245
|
+
*/
|
|
3155
3246
|
export interface QualityDelta {
|
|
3156
3247
|
moverId: string;
|
|
3157
3248
|
good: boolean;
|
|
3249
|
+
/** 처음에 통과한 수가 아니다 — 재작업해서 통과한 것이 여기 더해지면 직행수율이 품질률이 된다(§`FIRST_PASS_RULE`). */
|
|
3158
3250
|
goodCount: number;
|
|
3159
|
-
|
|
3251
|
+
/** **부적합 전체**(폐기 + 재작업 + 특채 …). ISO 22400 의 SQ 가 아니라 `SQ + RQ` 다 — 위 머리말. */
|
|
3252
|
+
nonconformingCount: number;
|
|
3160
3253
|
/**
|
|
3161
3254
|
* 이 산출이 어느 작업의 것인가 — **귀속이고, 수량의 뜻을 바꾸지 않는다**(설비 누적 그대로).
|
|
3162
3255
|
*
|
|
@@ -3176,11 +3269,26 @@ export interface QualityDelta {
|
|
|
3176
3269
|
* 표준 매핑은 커널이 이미 적어 두었다 — `MaterialDefinitionID` → `definitionId`,
|
|
3177
3270
|
* `MaterialUse` → `use`. 그 이름을 그대로 쓴다.
|
|
3178
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];
|
|
3179
3287
|
export interface MaterialActual {
|
|
3180
3288
|
/** 표준 `MaterialDefinitionID`. */
|
|
3181
3289
|
definitionId: string;
|
|
3182
|
-
/** 표준 `MaterialUse
|
|
3183
|
-
use:
|
|
3290
|
+
/** 표준 `MaterialUse` — 실적은 둘이다(§`MATERIAL_ACTUAL_USE`). */
|
|
3291
|
+
use: MaterialActualUse;
|
|
3184
3292
|
quantity: number;
|
|
3185
3293
|
uom?: string;
|
|
3186
3294
|
/**
|
|
@@ -3259,6 +3367,14 @@ export interface TaskStatusDelta {
|
|
|
3259
3367
|
export interface EquipmentStatusDelta extends EffectivePeriod {
|
|
3260
3368
|
moverId: string;
|
|
3261
3369
|
kind: string;
|
|
3370
|
+
/**
|
|
3371
|
+
* 속한 **등급들**(`EquipmentState.equipmentClassIds`). 나가지 않으면 미러가 소속을 영영 모른다 —
|
|
3372
|
+
* 상태⊆이벤트. 사람·자산 델타가 등급을 싣는 것과 같은 자리다.
|
|
3373
|
+
*
|
|
3374
|
+
* 그러면 등급으로 부르는 요구(`equipmentSpecification`)가 미러에서 채워지지 않고, **라인이 굶는데
|
|
3375
|
+
* 오류는 나지 않는다.**
|
|
3376
|
+
*/
|
|
3377
|
+
equipmentClassIds?: string[];
|
|
3262
3378
|
status: string;
|
|
3263
3379
|
location?: string;
|
|
3264
3380
|
/** 붙박인 자리(`EquipmentState.homeLocation`). 나가지 않으면 미러가 소속을 영영 모른다 — 상태⊆이벤트. */
|
|
@@ -3404,13 +3520,18 @@ export interface TwinModelDef {
|
|
|
3404
3520
|
testSpecificationIds?: TestSpecificationRefs;
|
|
3405
3521
|
})[];
|
|
3406
3522
|
/**
|
|
3407
|
-
* 설비
|
|
3408
|
-
* `window` 지정 시 그 시간대에만 일한다(교대·가동시간) — 미지정이면 24시간
|
|
3523
|
+
* 설비 — ISA-95 `Equipment`. mtbfMs/mttrMs 지정 시 확률적 고장 모델 참여(OEE Availability 손실).
|
|
3524
|
+
* 미지정=고장 없음. `window` 지정 시 그 시간대에만 일한다(교대·가동시간) — 미지정이면 24시간 가용.
|
|
3525
|
+
*
|
|
3526
|
+
* `equipmentClassIds` 로 **속한 등급들**을 밝힌다(복수가 표준 — 사람·자산과 같은 규칙). 선언하지
|
|
3527
|
+
* 않으면 `kind` 하나가 소속이다(기존 거동). 판정은 늘 `equipmentClassMembership` 을 거친다 —
|
|
3528
|
+
* 선언과 종류를 합치는 규칙이 한 자리에 있다.
|
|
3409
3529
|
*/
|
|
3410
3530
|
equipment: (EffectivePeriod & {
|
|
3411
3531
|
id: string;
|
|
3412
3532
|
name?: string;
|
|
3413
3533
|
kind: string;
|
|
3534
|
+
equipmentClassIds?: string[];
|
|
3414
3535
|
homeLocation: string;
|
|
3415
3536
|
identity?: string;
|
|
3416
3537
|
mtbfMs?: number;
|
|
@@ -3446,6 +3567,8 @@ export interface TwinModelDef {
|
|
|
3446
3567
|
assets?: (EffectivePeriod & {
|
|
3447
3568
|
id: string;
|
|
3448
3569
|
assetClassIds?: string[];
|
|
3570
|
+
ratedUsage?: RatedUsage;
|
|
3571
|
+
usedUsage?: UsedUsage;
|
|
3449
3572
|
homeLocation?: string;
|
|
3450
3573
|
properties?: ResourceProperty[];
|
|
3451
3574
|
testSpecificationIds?: TestSpecificationRefs;
|
package/dist/contract.js
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
* 지키는 하네스: `derived-not-input` · `actuals-come-from-transitions` · `accumulators-survive-restart` ·
|
|
33
33
|
* `purpose-conformance`.
|
|
34
34
|
*/
|
|
35
|
+
import { ratedUsageOf } from "./rated-usage.js";
|
|
35
36
|
/** 오더가 **종결 상태**로 인정되는 낱말 — 이 밖은 진행 중이다(§`isOrderTerminal`). */
|
|
36
37
|
export const ORDER_TERMINAL_STATUS = ['completed', 'cancelled'];
|
|
37
38
|
/**
|
|
@@ -503,6 +504,46 @@ export function classClosure(directIds, defs, at) {
|
|
|
503
504
|
}
|
|
504
505
|
return out;
|
|
505
506
|
}
|
|
507
|
+
/**
|
|
508
|
+
* 설비의 **등급 소속** — 선언한 등급들과 `kind` 를 합친 것.
|
|
509
|
+
*
|
|
510
|
+
* ── 왜 설비만 함수가 있나 ────────────────────────────────────────────────────
|
|
511
|
+
* 사람과 자산은 소속이 `personnelClassIds`·`assetClassIds` 하나뿐이라 부르는 쪽이 그 필드를 그대로
|
|
512
|
+
* 넘긴다. 설비는 소속이 **두 곳에서 온다** — 선언한 `equipmentClassIds` 와, 예전부터 소속 노릇을 해 온
|
|
513
|
+
* `kind`. 그 합침을 부르는 쪽마다 손으로 쓰면 한 곳이 빠진 날 그 설비는 **거기서만 등급을 잃는다.**
|
|
514
|
+
*
|
|
515
|
+
* 실제로 그 자리가 여섯이다: 스냅샷·가용 개수·배정 고르기·재수화·op 이벤트·관측 축소. 여섯이 같은
|
|
516
|
+
* 답을 내야 하므로 규칙이 한 자리에 있다(`requiredTestsFor` 가 계약에 있는 것과 같은 이유).
|
|
517
|
+
*
|
|
518
|
+
* ── `kind` 를 왜 소속으로 세나 (2026-09-03, 재고 결정) ──────────────────────
|
|
519
|
+
* 처음에는 설비 소속이 `kind` **하나**였다. 그것을 복수로 넓히면서 `kind` 를 뺄지 재 봤다.
|
|
520
|
+
*
|
|
521
|
+
* ```
|
|
522
|
+
* 저장된 트윈 32개 · kind 19종(cutter welder painter assembler hostler forklift meter pv-array …)
|
|
523
|
+
* equipmentClasses 를 선언한 트윈 0
|
|
524
|
+
* 설비를 이름으로 부르는 공정 52 그 이름이 전부 kind (cutter · welder · painter · assembler)
|
|
525
|
+
* ```
|
|
526
|
+
*
|
|
527
|
+
* **`kind` 는 지금 하중을 받고 있다.** 빼면 그 52 공정이 설비를 못 찾고, 요구가 채워지지 않아 라인이
|
|
528
|
+
* 굶는다 — 오류는 안 난다. 그리고 표준에서도 `EquipmentClass` 는 "정해진 목적으로 묶은 설비 무리" 이니
|
|
529
|
+
* 설비의 종류는 그 자체로 등급이다. 그래서 뺄 이유가 없고, **선언한 것에 더한다.**
|
|
530
|
+
*
|
|
531
|
+
* 합치는 것이 위험한 경우는 하나다 — 선언한 등급 id 가 어떤 `kind` 와 우연히 같을 때. 그때는 그 설비가
|
|
532
|
+
* 뜻하지 않은 요구를 만족한다. 다만 등급 id 는 사람이 일부러 짓는 이름이고 `kind` 도 그렇다. 같은
|
|
533
|
+
* 이름을 지었다면 같은 것을 뜻한 것으로 본다.
|
|
534
|
+
*
|
|
535
|
+
* 순서는 **선언한 것이 먼저**다 — 부르는 쪽이 첫 값을 대표 등급으로 쓸 때 종류가 아니라 선언이 대표가
|
|
536
|
+
* 된다. 중복은 접는다.
|
|
537
|
+
*/
|
|
538
|
+
export function equipmentClassMembership(e) {
|
|
539
|
+
const out = [];
|
|
540
|
+
for (const id of e?.equipmentClassIds ?? [])
|
|
541
|
+
if (id && !out.includes(id))
|
|
542
|
+
out.push(id);
|
|
543
|
+
if (e?.kind && !out.includes(e.kind))
|
|
544
|
+
out.push(e.kind);
|
|
545
|
+
return out;
|
|
546
|
+
}
|
|
506
547
|
/**
|
|
507
548
|
* 우선순위 — **ISA-95 `Priority`**(`JobOrderType`·`OperationsRequestType`, 타입은 `PriorityType` =
|
|
508
549
|
* `NumericType` 제한). 즉 표준은 **숫자라는 것만 정하고 방향은 정하지 않는다.**
|
|
@@ -910,6 +951,17 @@ export function capabilityOf(r, ctx) {
|
|
|
910
951
|
return { available: false, reason: 'held' };
|
|
911
952
|
if (r.status === 'down')
|
|
912
953
|
return { available: false, reason: 'down' };
|
|
954
|
+
/*
|
|
955
|
+
* 다 쓴 자원 — **고장보다 뒤, 교대보다 앞.**
|
|
956
|
+
*
|
|
957
|
+
* 고장이 먼저인 이유: 고장 난 것을 「수명 끝」이라고 말하면 고치러 가지 않는다. 교대보다 앞인
|
|
958
|
+
* 이유: 다 쓴 공구는 다음 교대가 와도 돌아오지 않는다 — 기다릴 일이 아니라 바꿀 일이다.
|
|
959
|
+
*
|
|
960
|
+
* **판정할 수 없으면 막지 않는다.** 정격을 선언하지 않았거나 쓴 양이 안 왔으면 `exhausted` 가
|
|
961
|
+
* 비어 있고, 그때 막으면 선언하지 않은 현장의 라인이 통째로 굶는다.
|
|
962
|
+
*/
|
|
963
|
+
if (ratedUsageOf(r).exhausted === true)
|
|
964
|
+
return { available: false, reason: 'worn-out' };
|
|
913
965
|
if (at) {
|
|
914
966
|
const ms = parsedMs(at);
|
|
915
967
|
if (Number.isFinite(ms)) {
|
|
@@ -1266,6 +1318,31 @@ export function isFailureStatus(value) {
|
|
|
1266
1318
|
export function isPlannedStopStatus(value) {
|
|
1267
1319
|
return normalizeEquipmentStatus(value) === 'planned-stop';
|
|
1268
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'];
|
|
1269
1346
|
// ── Command 채널 어휘 — 트윈의 "행위(act)" 면 (prescriptive/트랜잭션 프론트엔드) ──
|
|
1270
1347
|
// 코어 공통: order.hold/resume(할당 보류). 도메인: order.release(즉시 투입) 등은 handleCommand 로.
|
|
1271
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
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export * from './yms-profile.ts';
|
|
|
19
19
|
export * from './canonical-record.ts';
|
|
20
20
|
export * from './webhook.ts';
|
|
21
21
|
export * from './oee.ts';
|
|
22
|
+
export * from './rated-usage.ts';
|
|
22
23
|
export * from './yield.ts';
|
|
23
24
|
export * from './reliability.ts';
|
|
24
25
|
export * from './erp.ts';
|
package/dist/index.js
CHANGED
|
@@ -41,6 +41,7 @@ export * from "./canonical-record.js";
|
|
|
41
41
|
/* `webhook-signature.ts` 는 여기 없다 — `node:crypto` 를 쓰므로 `@operato/ops-contract/webhook` 으로만 나간다. */
|
|
42
42
|
export * from "./webhook.js";
|
|
43
43
|
export * from "./oee.js";
|
|
44
|
+
export * from "./rated-usage.js";
|
|
44
45
|
export * from "./yield.js";
|
|
45
46
|
export * from "./reliability.js";
|
|
46
47
|
export * from "./erp.js";
|
package/dist/oee.d.ts
CHANGED
|
@@ -11,8 +11,15 @@ export interface OeeCounters {
|
|
|
11
11
|
setupMs: number;
|
|
12
12
|
/** 고장(ADOT). */
|
|
13
13
|
downMs: number;
|
|
14
|
+
/** 양품(GQ). */
|
|
14
15
|
goodCount: number;
|
|
15
|
-
|
|
16
|
+
/**
|
|
17
|
+
* **부적합 전체**(GQ 가 아닌 것 전부) — ISO 22400 의 **SQ 가 아니고 `SQ + RQ`** 다.
|
|
18
|
+
*
|
|
19
|
+
* 품질률 `GQ / (GQ + SQ + RQ)` 의 분모가 `goodCount + nonconformingCount` 로 정확히 맞는다.
|
|
20
|
+
* 예전 이름은 `scrapCount` 였고 폐기 수로 읽혔다 — §`QualityDelta`. vocabulary-guard: allow
|
|
21
|
+
*/
|
|
22
|
+
nonconformingCount: number;
|
|
16
23
|
/** 계획정지 — 계획 조업 시간에서 뺀다(점심·예방보전·교대). */
|
|
17
24
|
holdMs?: number;
|
|
18
25
|
/** 언제부터 쟀나. 없으면 잰 구간이 없는 것이고, 그때는 가동률을 낼 수 없다. */
|
package/dist/oee.js
CHANGED
|
@@ -33,7 +33,7 @@ export function computeOee(c, nowMs) {
|
|
|
33
33
|
/* 대기는 남는 시간이다 — 계획 조업에서 생산·준비·고장을 뺀 것. 따로 재지 않아도 나온다. */
|
|
34
34
|
const delayMs = Math.max(0, planned - c.runMs - c.setupMs - c.downMs);
|
|
35
35
|
const availability = planned > 0 ? Math.min(1, c.runMs / planned) : undefined;
|
|
36
|
-
const produced = c.goodCount + c.
|
|
36
|
+
const produced = c.goodCount + c.nonconformingCount;
|
|
37
37
|
if (produced <= 0)
|
|
38
38
|
missing.push('produced-quantity');
|
|
39
39
|
const quality = produced > 0 ? c.goodCount / produced : undefined;
|
|
@@ -57,6 +57,6 @@ export function computeOee(c, nowMs) {
|
|
|
57
57
|
downMs: c.downMs,
|
|
58
58
|
delayMs,
|
|
59
59
|
goodCount: c.goodCount,
|
|
60
|
-
|
|
60
|
+
nonconformingCount: c.nonconformingCount
|
|
61
61
|
};
|
|
62
62
|
}
|
|
@@ -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'],
|
|
@@ -143,7 +216,7 @@ const SPECS = {
|
|
|
143
216
|
matchOrder: 15,
|
|
144
217
|
identity: 'moverId', // vocabulary-guard: allow 저널 와이어 필드
|
|
145
218
|
/* 누적 카운터가 없으면 OEE 가 양품률을 못 센다 — 판정 하나만으로는 비율이 나오지 않는다. */
|
|
146
|
-
required: ['moverId', 'good', 'goodCount', '
|
|
219
|
+
required: ['moverId', 'good', 'goodCount', 'nonconformingCount'], // vocabulary-guard: allow
|
|
147
220
|
/*
|
|
148
221
|
* `taskId` 는 **귀속**이다 — 이 산출이 어느 작업의 것인가. 수량의 뜻은 바뀌지 않는다(설비 누적).
|
|
149
222
|
*
|
|
@@ -153,7 +226,7 @@ const SPECS = {
|
|
|
153
226
|
* 정체를 `taskId` 로 바꾸지 않는 이유는 OEE 가 설비 단위여서다. 정체를 옮기면 설비 누적을 셀 수
|
|
154
227
|
* 없어진다.
|
|
155
228
|
*/
|
|
156
|
-
fields: { moverId: 'string', good: 'boolean', goodCount: 'number',
|
|
229
|
+
fields: { moverId: 'string', good: 'boolean', goodCount: 'number', nonconformingCount: 'number', taskId: 'string', recordTime: 'string' } // vocabulary-guard: allow
|
|
157
230
|
},
|
|
158
231
|
/*
|
|
159
232
|
* **시험 결과** — 대상을 가리켜 들어온다(표준 `TestResult.TestableObjectID`).
|
|
@@ -400,7 +473,7 @@ export function ingestOperationalRecords(records, opts) {
|
|
|
400
473
|
errors.push(`${kind}.progress 는 0~1 비율이다: ${n}`);
|
|
401
474
|
break;
|
|
402
475
|
}
|
|
403
|
-
if ((name === 'requested' || name === 'fulfilled' || name === 'goodCount' || name === '
|
|
476
|
+
if ((name === 'requested' || name === 'fulfilled' || name === 'goodCount' || name === 'nonconformingCount') && n < 0) {
|
|
404
477
|
errors.push(`${kind}.${name} 가 음수다: ${n}`);
|
|
405
478
|
break;
|
|
406
479
|
}
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** UN/CEFACT 권고 20 에서 **무차원 「하나」** — 「몇 번 썼나」의 단위. */
|
|
2
|
+
export declare const USE_UOM = "C62";
|
|
3
|
+
/** 정격 사용량 — 「얼마」와 「무엇으로」. */
|
|
4
|
+
export interface RatedUsage {
|
|
5
|
+
/** 이만큼 쓰면 교체한다. 0 이나 음수는 정격이 아니다(§`ratedUsageOf`). */
|
|
6
|
+
limit: number;
|
|
7
|
+
/** 단위 — UN/CEFACT 권고 20. 「번」은 `USE_UOM`(`C62`), 길이는 `MTR` 등. */
|
|
8
|
+
uom: string;
|
|
9
|
+
}
|
|
10
|
+
/** 지금까지 쓴 양 — 정격과 **같은 단위여야** 견줄 수 있다. */
|
|
11
|
+
export interface UsedUsage {
|
|
12
|
+
value: number;
|
|
13
|
+
uom: string;
|
|
14
|
+
/**
|
|
15
|
+
* 언제부터 센 것인가(ISO). 교정·오버홀이 자산을 처음 상태로 되돌리면 그 시각부터 다시 센다 —
|
|
16
|
+
* 되돌린 뒤에도 예전 양을 계속 세면 멀쩡한 지그가 정격 초과로 잠긴다.
|
|
17
|
+
*
|
|
18
|
+
* 없으면 「도입 이후 전체」로 읽는다. 그것은 짐작이 아니라 이 필드의 정의다.
|
|
19
|
+
*/
|
|
20
|
+
since?: string;
|
|
21
|
+
}
|
|
22
|
+
/** 정격으로 판정할 수 없는 이유 — 「남은 것 0」으로 답하지 않는다. */
|
|
23
|
+
export type RatedUsageMissing =
|
|
24
|
+
/** 정격을 선언하지 않았다. **무한이 아니라 모르는 것**이다 — 화면이 「제한 없음」으로 그리면 안 된다. */
|
|
25
|
+
'not-declared'
|
|
26
|
+
/** 정격이 0 이나 음수다 — 마스터가 잘못 적혔다. */
|
|
27
|
+
| 'invalid-limit'
|
|
28
|
+
/** 쓴 양이 아직 없다 — 원천이 보내 주지 않았거나 아직 안 썼다. 둘을 여기서 가리지 않는다. */
|
|
29
|
+
| 'usage-unknown'
|
|
30
|
+
/**
|
|
31
|
+
* 단위가 다르다 — **견주지 않는다.**
|
|
32
|
+
*
|
|
33
|
+
* 50미터 정격에 「120번 썼다」가 오면 답할 수 있는 것이 없다. 숫자만 견주면 120 > 50 이라
|
|
34
|
+
* 「수명 끝」이라고 말하는데, 그것은 참일 수도 거짓일 수도 있다.
|
|
35
|
+
*/
|
|
36
|
+
| 'uom-mismatch';
|
|
37
|
+
export interface RatedUsageStatus {
|
|
38
|
+
/** 정격 그대로. 없으면 없다. */
|
|
39
|
+
rated?: RatedUsage;
|
|
40
|
+
/** 쓴 양 그대로. 없으면 없다. */
|
|
41
|
+
used?: UsedUsage;
|
|
42
|
+
/** 남은 양 — **낼 수 없으면 없다.** */
|
|
43
|
+
remaining?: number;
|
|
44
|
+
/** 다 썼나 — 판정할 수 없으면 없다(`false` 가 아니다: 「아니다」와 「모른다」는 다르다). */
|
|
45
|
+
exhausted?: boolean;
|
|
46
|
+
/** 커널이 스스로 셀 수 있는 정격인가 — 「번」이면 참, 그 밖이면 거짓(§머리말). */
|
|
47
|
+
countable: boolean;
|
|
48
|
+
missing: RatedUsageMissing[];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 이 자원의 정격 사용 상태.
|
|
52
|
+
*
|
|
53
|
+
* **모르면 답하지 않는다.** 정격이 없으면 「무제한」이 아니고, 쓴 양이 없으면 「0」이 아니다. 둘 중
|
|
54
|
+
* 하나만 없어도 남은 양과 초과 여부를 내지 않는다 — 그때 화면은 「모른다」를 그려야 한다.
|
|
55
|
+
*/
|
|
56
|
+
export declare function ratedUsageOf(r: {
|
|
57
|
+
ratedUsage?: RatedUsage;
|
|
58
|
+
usedUsage?: UsedUsage;
|
|
59
|
+
}): RatedUsageStatus;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **정격 사용량** — 소모 공구가 얼마를 쓰면 교체되나.
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 필요한가 (2026-09-03, 미라텍 관리계획서) ─────────────────────────────
|
|
5
|
+
* 고객의 관리계획서가 공구마다 수명을 못 박아 둔다. 메탈마스크 5만 번, 스퀴즈 10만 번, EOL 프로브
|
|
6
|
+
* 핀 5만 번, 카메라 실리콘 패킹 10만 번, OQC 케이블 3만 번, **라우터 비트 50미터.**
|
|
7
|
+
*
|
|
8
|
+
* 트윈에 그 숫자를 담을 자리가 없었다. 그래서 정비 계획은 그것을 날수로 바꿔서 들고 있었다 —
|
|
9
|
+
* 「핀 5만 번 ÷ 하루 69대 = 720일」. 그 환산은 **생산량이 바뀌는 순간 틀리고**, 틀렸다는 것을
|
|
10
|
+
* 아무도 모른다. 라인을 증설하면 실제 수명은 반으로 줄지만 계획은 여전히 720일을 말한다.
|
|
11
|
+
*
|
|
12
|
+
* ── 단위를 함께 드는 이유 ────────────────────────────────────────────────────
|
|
13
|
+
* 여섯 중 다섯은 「번」이고 하나는 「미터」다. 숫자만 담는 칸에 라우터 비트를 넣으면 **50미터가
|
|
14
|
+
* 50번이 된다.** 한 번에 0.2미터를 깎는 공정이라면 실제 수명의 250분의 1에서 교체하라고 말하는
|
|
15
|
+
* 셈이다. 그래서 정격은 늘 「얼마」와 「무엇으로」 둘을 든다.
|
|
16
|
+
*
|
|
17
|
+
* 단위 어휘는 다른 자리와 같다 — UN/CEFACT 권고 20(§`MaterialQuantity.uom`).
|
|
18
|
+
*
|
|
19
|
+
* ── 커널이 스스로 셀 수 있는 경우와 없는 경우 ────────────────────────────────
|
|
20
|
+
* 정격이 「번」이면 작업이 끝날 때마다 하나 늘리면 된다. **「미터」면 늘릴 수 없다** — 그 작업이
|
|
21
|
+
* 몇 미터를 깎았는지는 공정이 말해 줘야 하는 사실이고, 커널이 「한 번이니 1미터」로 놓으면 그것은
|
|
22
|
+
* 지어낸 값이다.
|
|
23
|
+
*
|
|
24
|
+
* 그래서 커널은 셀 수 있는 것만 세고, 못 세는 것은 **못 센다고 말한다**(`countable`). 값이 오지
|
|
25
|
+
* 않으면 사용량은 비어 있고, 비어 있는 것은 0이 아니다.
|
|
26
|
+
*/
|
|
27
|
+
/** UN/CEFACT 권고 20 에서 **무차원 「하나」** — 「몇 번 썼나」의 단위. */
|
|
28
|
+
export const USE_UOM = 'C62';
|
|
29
|
+
const num = (n) => typeof n === 'number' && Number.isFinite(n);
|
|
30
|
+
/**
|
|
31
|
+
* 이 자원의 정격 사용 상태.
|
|
32
|
+
*
|
|
33
|
+
* **모르면 답하지 않는다.** 정격이 없으면 「무제한」이 아니고, 쓴 양이 없으면 「0」이 아니다. 둘 중
|
|
34
|
+
* 하나만 없어도 남은 양과 초과 여부를 내지 않는다 — 그때 화면은 「모른다」를 그려야 한다.
|
|
35
|
+
*/
|
|
36
|
+
export function ratedUsageOf(r) {
|
|
37
|
+
const rated = r?.ratedUsage;
|
|
38
|
+
const used = r?.usedUsage;
|
|
39
|
+
const missing = [];
|
|
40
|
+
/* 셀 수 있는지는 정격의 단위만으로 정해진다 — 쓴 양이 아직 없어도 답할 수 있다. */
|
|
41
|
+
const countable = rated?.uom === USE_UOM;
|
|
42
|
+
if (!rated)
|
|
43
|
+
missing.push('not-declared');
|
|
44
|
+
else if (!num(rated.limit) || rated.limit <= 0)
|
|
45
|
+
missing.push('invalid-limit');
|
|
46
|
+
if (!used || !num(used.value))
|
|
47
|
+
missing.push('usage-unknown');
|
|
48
|
+
if (rated && used && rated.uom !== used.uom)
|
|
49
|
+
missing.push('uom-mismatch');
|
|
50
|
+
const out = {
|
|
51
|
+
...(rated ? { rated } : {}),
|
|
52
|
+
...(used ? { used } : {}),
|
|
53
|
+
countable,
|
|
54
|
+
missing
|
|
55
|
+
};
|
|
56
|
+
if (missing.length)
|
|
57
|
+
return out;
|
|
58
|
+
const remaining = Math.max(0, rated.limit - used.value);
|
|
59
|
+
return { ...out, remaining, exhausted: used.value >= rated.limit };
|
|
60
|
+
}
|
package/dist/vocabulary.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* (문서 쪽은 `test/doc-vocabulary-guard.test.ts` 가 **자기 목록**으로 본다 — 이 목록의 `node`·`mover` 는
|
|
9
9
|
* ADR 기록이 없어 문서의 개념 명사에까지 들이대지 않는다.)
|
|
10
10
|
*/
|
|
11
|
-
export declare const RETIRED_VOCABULARY: readonly ["mover", "Mover", "MOVER", "node", "Node", "NODE", "BoardDef", "loadBoard"];
|
|
11
|
+
export declare const RETIRED_VOCABULARY: readonly ["mover", "Mover", "MOVER", "node", "Node", "NODE", "BoardDef", "loadBoard", "scrapCount"];
|
|
12
12
|
/**
|
|
13
13
|
* 예외 — 남아 있는 것이 **정당한** 토큰. 각 항목에 이유가 붙는다.
|
|
14
14
|
*
|
package/dist/vocabulary.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* (문서 쪽은 `test/doc-vocabulary-guard.test.ts` 가 **자기 목록**으로 본다 — 이 목록의 `node`·`mover` 는
|
|
21
21
|
* ADR 기록이 없어 문서의 개념 명사에까지 들이대지 않는다.)
|
|
22
22
|
*/
|
|
23
|
-
export const RETIRED_VOCABULARY = ['mover', 'Mover', 'MOVER', 'node', 'Node', 'NODE', 'BoardDef', 'loadBoard'];
|
|
23
|
+
export const RETIRED_VOCABULARY = ['mover', 'Mover', 'MOVER', 'node', 'Node', 'NODE', 'BoardDef', 'loadBoard', 'scrapCount'];
|
|
24
24
|
/**
|
|
25
25
|
* 예외 — 남아 있는 것이 **정당한** 토큰. 각 항목에 이유가 붙는다.
|
|
26
26
|
*
|
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,
|
|
@@ -66,6 +67,7 @@ __export(index_exports, {
|
|
|
66
67
|
TWIN_AXES: () => TWIN_AXES,
|
|
67
68
|
TWIN_PROPERTIES: () => TWIN_PROPERTIES,
|
|
68
69
|
TWIN_RELATIONS: () => TWIN_RELATIONS,
|
|
70
|
+
USE_UOM: () => USE_UOM,
|
|
69
71
|
UTC_OFFSET: () => UTC_OFFSET,
|
|
70
72
|
VOCABULARY_EXCEPTIONS: () => VOCABULARY_EXCEPTIONS,
|
|
71
73
|
VOCABULARY_TYPE: () => VOCABULARY_TYPE,
|
|
@@ -94,6 +96,7 @@ __export(index_exports, {
|
|
|
94
96
|
checkSequenceRun: () => checkSequenceRun,
|
|
95
97
|
classClosure: () => classClosure,
|
|
96
98
|
classIdentifierViolation: () => classIdentifierViolation,
|
|
99
|
+
classUri: () => classUri,
|
|
97
100
|
commandFromSpec: () => commandFromSpec,
|
|
98
101
|
commandSpecGaps: () => commandSpecGaps,
|
|
99
102
|
commandTypeOf: () => commandTypeOf,
|
|
@@ -107,6 +110,7 @@ __export(index_exports, {
|
|
|
107
110
|
dueStatusOf: () => dueStatusOf,
|
|
108
111
|
effectivityAt: () => effectivityAt,
|
|
109
112
|
electricalUpstreamOf: () => electricalUpstreamOf,
|
|
113
|
+
equipmentClassMembership: () => equipmentClassMembership,
|
|
110
114
|
erpCapabilityGaps: () => erpCapabilityGaps,
|
|
111
115
|
expiryFromAttributes: () => expiryFromAttributes,
|
|
112
116
|
gdtiUri: () => gdtiUri,
|
|
@@ -182,6 +186,7 @@ __export(index_exports, {
|
|
|
182
186
|
procedureViolations: () => procedureViolations,
|
|
183
187
|
propertiesOf: () => propertiesOf,
|
|
184
188
|
quantityIn: () => quantityIn,
|
|
189
|
+
ratedUsageOf: () => ratedUsageOf,
|
|
185
190
|
readBoardAssets: () => readBoardAssets,
|
|
186
191
|
readBoardEquipment: () => readBoardEquipment,
|
|
187
192
|
readBoardLocations: () => readBoardLocations,
|
|
@@ -352,6 +357,29 @@ function commandsOf(caps) {
|
|
|
352
357
|
return [...out];
|
|
353
358
|
}
|
|
354
359
|
|
|
360
|
+
// src/rated-usage.ts
|
|
361
|
+
var USE_UOM = "C62";
|
|
362
|
+
var num = (n) => typeof n === "number" && Number.isFinite(n);
|
|
363
|
+
function ratedUsageOf(r) {
|
|
364
|
+
const rated = r?.ratedUsage;
|
|
365
|
+
const used = r?.usedUsage;
|
|
366
|
+
const missing = [];
|
|
367
|
+
const countable = rated?.uom === USE_UOM;
|
|
368
|
+
if (!rated) missing.push("not-declared");
|
|
369
|
+
else if (!num(rated.limit) || rated.limit <= 0) missing.push("invalid-limit");
|
|
370
|
+
if (!used || !num(used.value)) missing.push("usage-unknown");
|
|
371
|
+
if (rated && used && rated.uom !== used.uom) missing.push("uom-mismatch");
|
|
372
|
+
const out = {
|
|
373
|
+
...rated ? { rated } : {},
|
|
374
|
+
...used ? { used } : {},
|
|
375
|
+
countable,
|
|
376
|
+
missing
|
|
377
|
+
};
|
|
378
|
+
if (missing.length) return out;
|
|
379
|
+
const remaining = Math.max(0, rated.limit - used.value);
|
|
380
|
+
return { ...out, remaining, exhausted: used.value >= rated.limit };
|
|
381
|
+
}
|
|
382
|
+
|
|
355
383
|
// src/contract.ts
|
|
356
384
|
var ORDER_TERMINAL_STATUS = ["completed", "cancelled"];
|
|
357
385
|
function isOrderTerminal(o) {
|
|
@@ -577,6 +605,12 @@ function classClosure(directIds, defs, at2) {
|
|
|
577
605
|
}
|
|
578
606
|
return out;
|
|
579
607
|
}
|
|
608
|
+
function equipmentClassMembership(e) {
|
|
609
|
+
const out = [];
|
|
610
|
+
for (const id of e?.equipmentClassIds ?? []) if (id && !out.includes(id)) out.push(id);
|
|
611
|
+
if (e?.kind && !out.includes(e.kind)) out.push(e.kind);
|
|
612
|
+
return out;
|
|
613
|
+
}
|
|
580
614
|
var PRIORITY_UNSET = Number.POSITIVE_INFINITY;
|
|
581
615
|
function priorityRank(p) {
|
|
582
616
|
return typeof p === "number" && Number.isFinite(p) ? p : PRIORITY_UNSET;
|
|
@@ -588,8 +622,8 @@ function dueStatusOf(x, nowIso) {
|
|
|
588
622
|
if (!Number.isFinite(due) || !Number.isFinite(now)) return void 0;
|
|
589
623
|
return now > due ? "late" : "on-time";
|
|
590
624
|
}
|
|
591
|
-
function subLotIdOf(
|
|
592
|
-
return `${
|
|
625
|
+
function subLotIdOf(classUri2, location) {
|
|
626
|
+
return `${classUri2}@${location}`;
|
|
593
627
|
}
|
|
594
628
|
function itemKeyOf(item) {
|
|
595
629
|
return item.subLotId ?? item.epc;
|
|
@@ -757,6 +791,7 @@ function capabilityOf(r, ctx) {
|
|
|
757
791
|
if (eff === "expired") return { available: false, reason: "retired" };
|
|
758
792
|
if (r.held) return { available: false, reason: "held" };
|
|
759
793
|
if (r.status === "down") return { available: false, reason: "down" };
|
|
794
|
+
if (ratedUsageOf(r).exhausted === true) return { available: false, reason: "worn-out" };
|
|
760
795
|
if (at2) {
|
|
761
796
|
const ms = parsedMs(at2);
|
|
762
797
|
if (Number.isFinite(ms)) {
|
|
@@ -999,6 +1034,7 @@ function isFailureStatus(value) {
|
|
|
999
1034
|
function isPlannedStopStatus(value) {
|
|
1000
1035
|
return normalizeEquipmentStatus(value) === "planned-stop";
|
|
1001
1036
|
}
|
|
1037
|
+
var MATERIAL_ACTUAL_USE = ["consumed", "produced"];
|
|
1002
1038
|
var CMD = {
|
|
1003
1039
|
orderHold: "order.hold",
|
|
1004
1040
|
orderResume: "order.resume",
|
|
@@ -2070,7 +2106,7 @@ function ingestEnergyRecords(records, opts) {
|
|
|
2070
2106
|
const at2 = String(record?.at ?? "").trim() || opts.defaultEventTime;
|
|
2071
2107
|
const atMs = at2 ? Date.parse(at2) : Number.NaN;
|
|
2072
2108
|
if (!Number.isFinite(atMs)) errors.push("at \uC5C6\uC74C/\uD615\uC2DD \uC624\uB958 \u2014 \uC9C0\uAE08 \uC2DC\uAC01\uC73C\uB85C \uBA54\uC6B0\uBA74 \uB0A8\uC758 \uC218\uC694 \uAD6C\uAC04\uC5D0 \uC2E4\uB9B0\uB2E4");
|
|
2073
|
-
const
|
|
2109
|
+
const num2 = (v, name) => {
|
|
2074
2110
|
if (v === void 0 || v === null || v === "") return void 0;
|
|
2075
2111
|
const n = Number(v);
|
|
2076
2112
|
if (!Number.isFinite(n)) {
|
|
@@ -2079,9 +2115,9 @@ function ingestEnergyRecords(records, opts) {
|
|
|
2079
2115
|
}
|
|
2080
2116
|
return n;
|
|
2081
2117
|
};
|
|
2082
|
-
const kW =
|
|
2083
|
-
const kWh =
|
|
2084
|
-
const powerFactor =
|
|
2118
|
+
const kW = num2(record?.kW, "kW");
|
|
2119
|
+
const kWh = num2(record?.kWh, "kWh");
|
|
2120
|
+
const powerFactor = num2(record?.powerFactor, "powerFactor");
|
|
2085
2121
|
if (errors.length) {
|
|
2086
2122
|
rejected.push({ record, errors });
|
|
2087
2123
|
continue;
|
|
@@ -2126,7 +2162,7 @@ function ingestEnergyEquipmentRecords(records, opts) {
|
|
|
2126
2162
|
const at2 = String(r?.at ?? "").trim() || opts.defaultEventTime;
|
|
2127
2163
|
const atMs = at2 ? Date.parse(at2) : Number.NaN;
|
|
2128
2164
|
if (!Number.isFinite(atMs)) errors.push("at \uC5C6\uC74C/\uD615\uC2DD \uC624\uB958 \u2014 \uC9C0\uAE08 \uC2DC\uAC01\uC73C\uB85C \uBA54\uC6B0\uBA74 \uC5B8\uC81C\uC758 \uC0C1\uD0DC\uC778\uC9C0 \uC54C \uC218 \uC5C6\uB2E4");
|
|
2129
|
-
const
|
|
2165
|
+
const num2 = (v, name, min, max) => {
|
|
2130
2166
|
if (v === void 0 || v === null || v === "") return void 0;
|
|
2131
2167
|
const n = Number(v);
|
|
2132
2168
|
if (!Number.isFinite(n)) {
|
|
@@ -2143,19 +2179,19 @@ function ingestEnergyEquipmentRecords(records, opts) {
|
|
|
2143
2179
|
}
|
|
2144
2180
|
return n;
|
|
2145
2181
|
};
|
|
2146
|
-
const generatedKW =
|
|
2147
|
-
const exportKW =
|
|
2148
|
-
const soc =
|
|
2149
|
-
const chargeKW =
|
|
2150
|
-
const dischargeKW =
|
|
2151
|
-
const minKW =
|
|
2182
|
+
const generatedKW = num2(r?.generatedKW, "generatedKW", 0);
|
|
2183
|
+
const exportKW = num2(r?.exportKW, "exportKW");
|
|
2184
|
+
const soc = num2(r?.soc, "soc", 0, 100);
|
|
2185
|
+
const chargeKW = num2(r?.chargeKW, "chargeKW", 0);
|
|
2186
|
+
const dischargeKW = num2(r?.dischargeKW, "dischargeKW", 0);
|
|
2187
|
+
const minKW = num2(r?.minKW, "minKW", 0);
|
|
2152
2188
|
let curtailable;
|
|
2153
2189
|
if (r?.curtailable !== void 0 && r?.curtailable !== null) {
|
|
2154
2190
|
if (typeof r.curtailable !== "boolean") errors.push(`curtailable \uAC00 \uCC38/\uAC70\uC9D3\uC774 \uC544\uB2C8\uB2E4: ${JSON.stringify(r.curtailable)}`);
|
|
2155
2191
|
else curtailable = r.curtailable;
|
|
2156
2192
|
}
|
|
2157
|
-
const dcVoltage =
|
|
2158
|
-
const dcCurrent =
|
|
2193
|
+
const dcVoltage = num2(r?.dcVoltage, "dcVoltage", 0);
|
|
2194
|
+
const dcCurrent = num2(r?.dcCurrent, "dcCurrent", 0);
|
|
2159
2195
|
const phases = (raw, name) => {
|
|
2160
2196
|
if (raw === void 0 || raw === null) return void 0;
|
|
2161
2197
|
if (!Array.isArray(raw)) {
|
|
@@ -2758,14 +2794,27 @@ function gdtiUri(companyPrefix, docType, serial) {
|
|
|
2758
2794
|
function objectUri(namespace, objId) {
|
|
2759
2795
|
return underNamespace(namespace, "obj", objId);
|
|
2760
2796
|
}
|
|
2797
|
+
function classUri(namespace, classId) {
|
|
2798
|
+
return underNamespace(namespace, "class", classId);
|
|
2799
|
+
}
|
|
2761
2800
|
function bizTransactionUri(namespace, transId) {
|
|
2762
2801
|
return underNamespace(namespace, "bt", transId);
|
|
2763
2802
|
}
|
|
2803
|
+
var STRUCTURE_BREAKING = /[%/:?# ]/g;
|
|
2804
|
+
var PERCENT_ENCODED = {
|
|
2805
|
+
"%": "%25",
|
|
2806
|
+
"/": "%2F",
|
|
2807
|
+
":": "%3A",
|
|
2808
|
+
"?": "%3F",
|
|
2809
|
+
"#": "%23",
|
|
2810
|
+
" ": "%20"
|
|
2811
|
+
};
|
|
2764
2812
|
function underNamespace(namespace, marker, id) {
|
|
2765
2813
|
const ns = namespace?.trim();
|
|
2766
2814
|
if (!ns) return void 0;
|
|
2767
|
-
const
|
|
2768
|
-
if (!
|
|
2815
|
+
const raw = String(id);
|
|
2816
|
+
if (!raw) return void 0;
|
|
2817
|
+
const v = raw.replace(STRUCTURE_BREAKING, (c) => PERCENT_ENCODED[c]);
|
|
2769
2818
|
if (/^https?:\/\/[^/\s]+/.test(ns)) return `${ns.replace(/\/+$/, "")}/${marker}/${v}`;
|
|
2770
2819
|
if (/^urn:epc(global)?:/.test(ns)) return void 0;
|
|
2771
2820
|
if (/^urn:[^:\s]+/.test(ns)) return `${ns.replace(/:+$/, "")}:${marker}:${v}`;
|
|
@@ -2844,7 +2893,7 @@ var EPC_CLASS_PREFIXES = ["urn:epc:idpat:", "urn:epc:class:"];
|
|
|
2844
2893
|
var DL_CANONICAL = "https://id.gs1.org/";
|
|
2845
2894
|
var CBV_URL_CLASS = /^https?:\/\/[^/\s]+\/(?:[^/\s]+\/)*class\/[^/\s]+$/;
|
|
2846
2895
|
var CBV_URN_CLASS = /^urn:[^:\s]+:(?:[^:\s]+:)*class:[^:\s]+$/;
|
|
2847
|
-
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 };
|
|
2848
2897
|
function gs1KeyDigitViolation(uri) {
|
|
2849
2898
|
if (!uri) return void 0;
|
|
2850
2899
|
const m = /^urn:epc:(?:id|idpat|class):([a-z]+):([^:]+)$/.exec(uri);
|
|
@@ -3224,6 +3273,39 @@ function readEpochMs(raw) {
|
|
|
3224
3273
|
}
|
|
3225
3274
|
|
|
3226
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
|
+
}
|
|
3227
3309
|
var TASK_STATUS = ["created", "assigned", "in-progress", "completed"];
|
|
3228
3310
|
var EQUIPMENT_STATUS2 = ["idle", "busy", "down", "setup", "planned-stop"];
|
|
3229
3311
|
var PERSON_STATUS = ["idle", "busy"];
|
|
@@ -3260,6 +3342,27 @@ var SPECS = {
|
|
|
3260
3342
|
materialActual: "object[]",
|
|
3261
3343
|
recordTime: "string"
|
|
3262
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
|
+
},
|
|
3263
3366
|
enums: {
|
|
3264
3367
|
status: TASK_STATUS,
|
|
3265
3368
|
intent: ["transport", "process", "dwell"],
|
|
@@ -3371,7 +3474,7 @@ var SPECS = {
|
|
|
3371
3474
|
identity: "moverId",
|
|
3372
3475
|
// vocabulary-guard: allow 저널 와이어 필드
|
|
3373
3476
|
/* 누적 카운터가 없으면 OEE 가 양품률을 못 센다 — 판정 하나만으로는 비율이 나오지 않는다. */
|
|
3374
|
-
required: ["moverId", "good", "goodCount", "
|
|
3477
|
+
required: ["moverId", "good", "goodCount", "nonconformingCount"],
|
|
3375
3478
|
// vocabulary-guard: allow
|
|
3376
3479
|
/*
|
|
3377
3480
|
* `taskId` 는 **귀속**이다 — 이 산출이 어느 작업의 것인가. 수량의 뜻은 바뀌지 않는다(설비 누적).
|
|
@@ -3382,7 +3485,7 @@ var SPECS = {
|
|
|
3382
3485
|
* 정체를 `taskId` 로 바꾸지 않는 이유는 OEE 가 설비 단위여서다. 정체를 옮기면 설비 누적을 셀 수
|
|
3383
3486
|
* 없어진다.
|
|
3384
3487
|
*/
|
|
3385
|
-
fields: { moverId: "string", good: "boolean", goodCount: "number",
|
|
3488
|
+
fields: { moverId: "string", good: "boolean", goodCount: "number", nonconformingCount: "number", taskId: "string", recordTime: "string" }
|
|
3386
3489
|
// vocabulary-guard: allow
|
|
3387
3490
|
},
|
|
3388
3491
|
/*
|
|
@@ -3614,7 +3717,7 @@ function ingestOperationalRecords(records, opts) {
|
|
|
3614
3717
|
errors.push(`${kind}.progress \uB294 0~1 \uBE44\uC728\uC774\uB2E4: ${n}`);
|
|
3615
3718
|
break;
|
|
3616
3719
|
}
|
|
3617
|
-
if ((name === "requested" || name === "fulfilled" || name === "goodCount" || name === "
|
|
3720
|
+
if ((name === "requested" || name === "fulfilled" || name === "goodCount" || name === "nonconformingCount") && n < 0) {
|
|
3618
3721
|
errors.push(`${kind}.${name} \uAC00 \uC74C\uC218\uB2E4: ${n}`);
|
|
3619
3722
|
break;
|
|
3620
3723
|
}
|
|
@@ -3642,6 +3745,7 @@ function ingestOperationalRecords(records, opts) {
|
|
|
3642
3745
|
errors.push(`${kind}.${name} \uAC00 \uAC1D\uCCB4\uAC00 \uC544\uB2C8\uB2E4: ${JSON.stringify(v)}`);
|
|
3643
3746
|
break;
|
|
3644
3747
|
}
|
|
3748
|
+
errors.push(...shapeErrors(`${kind}.${name}`, v, spec.shapes?.[name]));
|
|
3645
3749
|
data[name] = { ...v };
|
|
3646
3750
|
break;
|
|
3647
3751
|
}
|
|
@@ -3650,6 +3754,9 @@ function ingestOperationalRecords(records, opts) {
|
|
|
3650
3754
|
errors.push(`${kind}.${name} \uAC00 \uAC1D\uCCB4 \uBC30\uC5F4\uC774 \uC544\uB2C8\uB2E4: ${JSON.stringify(v)}`);
|
|
3651
3755
|
break;
|
|
3652
3756
|
}
|
|
3757
|
+
v.forEach(
|
|
3758
|
+
(x, i) => errors.push(...shapeErrors(`${kind}.${name}[${i}]`, x, spec.shapes?.[name]))
|
|
3759
|
+
);
|
|
3653
3760
|
data[name] = v.map((x) => ({ ...x }));
|
|
3654
3761
|
break;
|
|
3655
3762
|
}
|
|
@@ -3790,7 +3897,7 @@ function validateScenario(def) {
|
|
|
3790
3897
|
}
|
|
3791
3898
|
|
|
3792
3899
|
// src/vocabulary.ts
|
|
3793
|
-
var RETIRED_VOCABULARY = ["mover", "Mover", "MOVER", "node", "Node", "NODE", "BoardDef", "loadBoard"];
|
|
3900
|
+
var RETIRED_VOCABULARY = ["mover", "Mover", "MOVER", "node", "Node", "NODE", "BoardDef", "loadBoard", "scrapCount"];
|
|
3794
3901
|
var VOCABULARY_EXCEPTIONS = [
|
|
3795
3902
|
/* ── 저널 와이어 필드 — append-only 역사이므로 이름을 바꾸지 않는다 ───────
|
|
3796
3903
|
* 같은 사실이 시점에 따라 다른 키로 들어가면 낡은 이름보다 나쁘다. 개명은 이벤트 스키마
|
|
@@ -3894,7 +4001,7 @@ function computeOee(c, nowMs) {
|
|
|
3894
4001
|
if (planned <= 0) missing.push("planned-busy-time");
|
|
3895
4002
|
const delayMs = Math.max(0, planned - c.runMs - c.setupMs - c.downMs);
|
|
3896
4003
|
const availability = planned > 0 ? Math.min(1, c.runMs / planned) : void 0;
|
|
3897
|
-
const produced = c.goodCount + c.
|
|
4004
|
+
const produced = c.goodCount + c.nonconformingCount;
|
|
3898
4005
|
if (produced <= 0) missing.push("produced-quantity");
|
|
3899
4006
|
const quality = produced > 0 ? c.goodCount / produced : void 0;
|
|
3900
4007
|
if (c.plannedRunTimePerItemMs == null) missing.push("planned-run-time-per-item");
|
|
@@ -3912,7 +4019,7 @@ function computeOee(c, nowMs) {
|
|
|
3912
4019
|
downMs: c.downMs,
|
|
3913
4020
|
delayMs,
|
|
3914
4021
|
goodCount: c.goodCount,
|
|
3915
|
-
|
|
4022
|
+
nonconformingCount: c.nonconformingCount
|
|
3916
4023
|
};
|
|
3917
4024
|
}
|
|
3918
4025
|
|
|
@@ -4200,6 +4307,7 @@ function commandSpecGaps(specs, command) {
|
|
|
4200
4307
|
GUARD_PRAGMA,
|
|
4201
4308
|
ILMD_ATTR,
|
|
4202
4309
|
LOCATION_SATURATION_NEAR,
|
|
4310
|
+
MATERIAL_ACTUAL_USE,
|
|
4203
4311
|
MATERIAL_PROPERTY,
|
|
4204
4312
|
MES_BIZSTEP,
|
|
4205
4313
|
MES_COMMANDS,
|
|
@@ -4216,6 +4324,7 @@ function commandSpecGaps(specs, command) {
|
|
|
4216
4324
|
TWIN_AXES,
|
|
4217
4325
|
TWIN_PROPERTIES,
|
|
4218
4326
|
TWIN_RELATIONS,
|
|
4327
|
+
USE_UOM,
|
|
4219
4328
|
UTC_OFFSET,
|
|
4220
4329
|
VOCABULARY_EXCEPTIONS,
|
|
4221
4330
|
VOCABULARY_TYPE,
|
|
@@ -4244,6 +4353,7 @@ function commandSpecGaps(specs, command) {
|
|
|
4244
4353
|
checkSequenceRun,
|
|
4245
4354
|
classClosure,
|
|
4246
4355
|
classIdentifierViolation,
|
|
4356
|
+
classUri,
|
|
4247
4357
|
commandFromSpec,
|
|
4248
4358
|
commandSpecGaps,
|
|
4249
4359
|
commandTypeOf,
|
|
@@ -4257,6 +4367,7 @@ function commandSpecGaps(specs, command) {
|
|
|
4257
4367
|
dueStatusOf,
|
|
4258
4368
|
effectivityAt,
|
|
4259
4369
|
electricalUpstreamOf,
|
|
4370
|
+
equipmentClassMembership,
|
|
4260
4371
|
erpCapabilityGaps,
|
|
4261
4372
|
expiryFromAttributes,
|
|
4262
4373
|
gdtiUri,
|
|
@@ -4332,6 +4443,7 @@ function commandSpecGaps(specs, command) {
|
|
|
4332
4443
|
procedureViolations,
|
|
4333
4444
|
propertiesOf,
|
|
4334
4445
|
quantityIn,
|
|
4446
|
+
ratedUsageOf,
|
|
4335
4447
|
readBoardAssets,
|
|
4336
4448
|
readBoardEquipment,
|
|
4337
4449
|
readBoardLocations,
|
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",
|