@operato/ops-contract 0.9.1 → 0.9.3
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 +113 -5
- package/dist/contract.js +52 -0
- 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.js +3 -3
- 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 +55 -19
- 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
|
*
|
|
@@ -3259,6 +3352,14 @@ export interface TaskStatusDelta {
|
|
|
3259
3352
|
export interface EquipmentStatusDelta extends EffectivePeriod {
|
|
3260
3353
|
moverId: string;
|
|
3261
3354
|
kind: string;
|
|
3355
|
+
/**
|
|
3356
|
+
* 속한 **등급들**(`EquipmentState.equipmentClassIds`). 나가지 않으면 미러가 소속을 영영 모른다 —
|
|
3357
|
+
* 상태⊆이벤트. 사람·자산 델타가 등급을 싣는 것과 같은 자리다.
|
|
3358
|
+
*
|
|
3359
|
+
* 그러면 등급으로 부르는 요구(`equipmentSpecification`)가 미러에서 채워지지 않고, **라인이 굶는데
|
|
3360
|
+
* 오류는 나지 않는다.**
|
|
3361
|
+
*/
|
|
3362
|
+
equipmentClassIds?: string[];
|
|
3262
3363
|
status: string;
|
|
3263
3364
|
location?: string;
|
|
3264
3365
|
/** 붙박인 자리(`EquipmentState.homeLocation`). 나가지 않으면 미러가 소속을 영영 모른다 — 상태⊆이벤트. */
|
|
@@ -3404,13 +3505,18 @@ export interface TwinModelDef {
|
|
|
3404
3505
|
testSpecificationIds?: TestSpecificationRefs;
|
|
3405
3506
|
})[];
|
|
3406
3507
|
/**
|
|
3407
|
-
* 설비
|
|
3408
|
-
* `window` 지정 시 그 시간대에만 일한다(교대·가동시간) — 미지정이면 24시간
|
|
3508
|
+
* 설비 — ISA-95 `Equipment`. mtbfMs/mttrMs 지정 시 확률적 고장 모델 참여(OEE Availability 손실).
|
|
3509
|
+
* 미지정=고장 없음. `window` 지정 시 그 시간대에만 일한다(교대·가동시간) — 미지정이면 24시간 가용.
|
|
3510
|
+
*
|
|
3511
|
+
* `equipmentClassIds` 로 **속한 등급들**을 밝힌다(복수가 표준 — 사람·자산과 같은 규칙). 선언하지
|
|
3512
|
+
* 않으면 `kind` 하나가 소속이다(기존 거동). 판정은 늘 `equipmentClassMembership` 을 거친다 —
|
|
3513
|
+
* 선언과 종류를 합치는 규칙이 한 자리에 있다.
|
|
3409
3514
|
*/
|
|
3410
3515
|
equipment: (EffectivePeriod & {
|
|
3411
3516
|
id: string;
|
|
3412
3517
|
name?: string;
|
|
3413
3518
|
kind: string;
|
|
3519
|
+
equipmentClassIds?: string[];
|
|
3414
3520
|
homeLocation: string;
|
|
3415
3521
|
identity?: string;
|
|
3416
3522
|
mtbfMs?: number;
|
|
@@ -3446,6 +3552,8 @@ export interface TwinModelDef {
|
|
|
3446
3552
|
assets?: (EffectivePeriod & {
|
|
3447
3553
|
id: string;
|
|
3448
3554
|
assetClassIds?: string[];
|
|
3555
|
+
ratedUsage?: RatedUsage;
|
|
3556
|
+
usedUsage?: UsedUsage;
|
|
3449
3557
|
homeLocation?: string;
|
|
3450
3558
|
properties?: ResourceProperty[];
|
|
3451
3559
|
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)) {
|
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
|
}
|
|
@@ -143,7 +143,7 @@ const SPECS = {
|
|
|
143
143
|
matchOrder: 15,
|
|
144
144
|
identity: 'moverId', // vocabulary-guard: allow 저널 와이어 필드
|
|
145
145
|
/* 누적 카운터가 없으면 OEE 가 양품률을 못 센다 — 판정 하나만으로는 비율이 나오지 않는다. */
|
|
146
|
-
required: ['moverId', 'good', 'goodCount', '
|
|
146
|
+
required: ['moverId', 'good', 'goodCount', 'nonconformingCount'], // vocabulary-guard: allow
|
|
147
147
|
/*
|
|
148
148
|
* `taskId` 는 **귀속**이다 — 이 산출이 어느 작업의 것인가. 수량의 뜻은 바뀌지 않는다(설비 누적).
|
|
149
149
|
*
|
|
@@ -153,7 +153,7 @@ const SPECS = {
|
|
|
153
153
|
* 정체를 `taskId` 로 바꾸지 않는 이유는 OEE 가 설비 단위여서다. 정체를 옮기면 설비 누적을 셀 수
|
|
154
154
|
* 없어진다.
|
|
155
155
|
*/
|
|
156
|
-
fields: { moverId: 'string', good: 'boolean', goodCount: 'number',
|
|
156
|
+
fields: { moverId: 'string', good: 'boolean', goodCount: 'number', nonconformingCount: 'number', taskId: 'string', recordTime: 'string' } // vocabulary-guard: allow
|
|
157
157
|
},
|
|
158
158
|
/*
|
|
159
159
|
* **시험 결과** — 대상을 가리켜 들어온다(표준 `TestResult.TestableObjectID`).
|
|
@@ -400,7 +400,7 @@ export function ingestOperationalRecords(records, opts) {
|
|
|
400
400
|
errors.push(`${kind}.progress 는 0~1 비율이다: ${n}`);
|
|
401
401
|
break;
|
|
402
402
|
}
|
|
403
|
-
if ((name === 'requested' || name === 'fulfilled' || name === 'goodCount' || name === '
|
|
403
|
+
if ((name === 'requested' || name === 'fulfilled' || name === 'goodCount' || name === 'nonconformingCount') && n < 0) {
|
|
404
404
|
errors.push(`${kind}.${name} 가 음수다: ${n}`);
|
|
405
405
|
break;
|
|
406
406
|
}
|
|
@@ -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
|
@@ -66,6 +66,7 @@ __export(index_exports, {
|
|
|
66
66
|
TWIN_AXES: () => TWIN_AXES,
|
|
67
67
|
TWIN_PROPERTIES: () => TWIN_PROPERTIES,
|
|
68
68
|
TWIN_RELATIONS: () => TWIN_RELATIONS,
|
|
69
|
+
USE_UOM: () => USE_UOM,
|
|
69
70
|
UTC_OFFSET: () => UTC_OFFSET,
|
|
70
71
|
VOCABULARY_EXCEPTIONS: () => VOCABULARY_EXCEPTIONS,
|
|
71
72
|
VOCABULARY_TYPE: () => VOCABULARY_TYPE,
|
|
@@ -107,6 +108,7 @@ __export(index_exports, {
|
|
|
107
108
|
dueStatusOf: () => dueStatusOf,
|
|
108
109
|
effectivityAt: () => effectivityAt,
|
|
109
110
|
electricalUpstreamOf: () => electricalUpstreamOf,
|
|
111
|
+
equipmentClassMembership: () => equipmentClassMembership,
|
|
110
112
|
erpCapabilityGaps: () => erpCapabilityGaps,
|
|
111
113
|
expiryFromAttributes: () => expiryFromAttributes,
|
|
112
114
|
gdtiUri: () => gdtiUri,
|
|
@@ -182,6 +184,7 @@ __export(index_exports, {
|
|
|
182
184
|
procedureViolations: () => procedureViolations,
|
|
183
185
|
propertiesOf: () => propertiesOf,
|
|
184
186
|
quantityIn: () => quantityIn,
|
|
187
|
+
ratedUsageOf: () => ratedUsageOf,
|
|
185
188
|
readBoardAssets: () => readBoardAssets,
|
|
186
189
|
readBoardEquipment: () => readBoardEquipment,
|
|
187
190
|
readBoardLocations: () => readBoardLocations,
|
|
@@ -352,6 +355,29 @@ function commandsOf(caps) {
|
|
|
352
355
|
return [...out];
|
|
353
356
|
}
|
|
354
357
|
|
|
358
|
+
// src/rated-usage.ts
|
|
359
|
+
var USE_UOM = "C62";
|
|
360
|
+
var num = (n) => typeof n === "number" && Number.isFinite(n);
|
|
361
|
+
function ratedUsageOf(r) {
|
|
362
|
+
const rated = r?.ratedUsage;
|
|
363
|
+
const used = r?.usedUsage;
|
|
364
|
+
const missing = [];
|
|
365
|
+
const countable = rated?.uom === USE_UOM;
|
|
366
|
+
if (!rated) missing.push("not-declared");
|
|
367
|
+
else if (!num(rated.limit) || rated.limit <= 0) missing.push("invalid-limit");
|
|
368
|
+
if (!used || !num(used.value)) missing.push("usage-unknown");
|
|
369
|
+
if (rated && used && rated.uom !== used.uom) missing.push("uom-mismatch");
|
|
370
|
+
const out = {
|
|
371
|
+
...rated ? { rated } : {},
|
|
372
|
+
...used ? { used } : {},
|
|
373
|
+
countable,
|
|
374
|
+
missing
|
|
375
|
+
};
|
|
376
|
+
if (missing.length) return out;
|
|
377
|
+
const remaining = Math.max(0, rated.limit - used.value);
|
|
378
|
+
return { ...out, remaining, exhausted: used.value >= rated.limit };
|
|
379
|
+
}
|
|
380
|
+
|
|
355
381
|
// src/contract.ts
|
|
356
382
|
var ORDER_TERMINAL_STATUS = ["completed", "cancelled"];
|
|
357
383
|
function isOrderTerminal(o) {
|
|
@@ -577,6 +603,12 @@ function classClosure(directIds, defs, at2) {
|
|
|
577
603
|
}
|
|
578
604
|
return out;
|
|
579
605
|
}
|
|
606
|
+
function equipmentClassMembership(e) {
|
|
607
|
+
const out = [];
|
|
608
|
+
for (const id of e?.equipmentClassIds ?? []) if (id && !out.includes(id)) out.push(id);
|
|
609
|
+
if (e?.kind && !out.includes(e.kind)) out.push(e.kind);
|
|
610
|
+
return out;
|
|
611
|
+
}
|
|
580
612
|
var PRIORITY_UNSET = Number.POSITIVE_INFINITY;
|
|
581
613
|
function priorityRank(p) {
|
|
582
614
|
return typeof p === "number" && Number.isFinite(p) ? p : PRIORITY_UNSET;
|
|
@@ -757,6 +789,7 @@ function capabilityOf(r, ctx) {
|
|
|
757
789
|
if (eff === "expired") return { available: false, reason: "retired" };
|
|
758
790
|
if (r.held) return { available: false, reason: "held" };
|
|
759
791
|
if (r.status === "down") return { available: false, reason: "down" };
|
|
792
|
+
if (ratedUsageOf(r).exhausted === true) return { available: false, reason: "worn-out" };
|
|
760
793
|
if (at2) {
|
|
761
794
|
const ms = parsedMs(at2);
|
|
762
795
|
if (Number.isFinite(ms)) {
|
|
@@ -2070,7 +2103,7 @@ function ingestEnergyRecords(records, opts) {
|
|
|
2070
2103
|
const at2 = String(record?.at ?? "").trim() || opts.defaultEventTime;
|
|
2071
2104
|
const atMs = at2 ? Date.parse(at2) : Number.NaN;
|
|
2072
2105
|
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
|
|
2106
|
+
const num2 = (v, name) => {
|
|
2074
2107
|
if (v === void 0 || v === null || v === "") return void 0;
|
|
2075
2108
|
const n = Number(v);
|
|
2076
2109
|
if (!Number.isFinite(n)) {
|
|
@@ -2079,9 +2112,9 @@ function ingestEnergyRecords(records, opts) {
|
|
|
2079
2112
|
}
|
|
2080
2113
|
return n;
|
|
2081
2114
|
};
|
|
2082
|
-
const kW =
|
|
2083
|
-
const kWh =
|
|
2084
|
-
const powerFactor =
|
|
2115
|
+
const kW = num2(record?.kW, "kW");
|
|
2116
|
+
const kWh = num2(record?.kWh, "kWh");
|
|
2117
|
+
const powerFactor = num2(record?.powerFactor, "powerFactor");
|
|
2085
2118
|
if (errors.length) {
|
|
2086
2119
|
rejected.push({ record, errors });
|
|
2087
2120
|
continue;
|
|
@@ -2126,7 +2159,7 @@ function ingestEnergyEquipmentRecords(records, opts) {
|
|
|
2126
2159
|
const at2 = String(r?.at ?? "").trim() || opts.defaultEventTime;
|
|
2127
2160
|
const atMs = at2 ? Date.parse(at2) : Number.NaN;
|
|
2128
2161
|
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
|
|
2162
|
+
const num2 = (v, name, min, max) => {
|
|
2130
2163
|
if (v === void 0 || v === null || v === "") return void 0;
|
|
2131
2164
|
const n = Number(v);
|
|
2132
2165
|
if (!Number.isFinite(n)) {
|
|
@@ -2143,19 +2176,19 @@ function ingestEnergyEquipmentRecords(records, opts) {
|
|
|
2143
2176
|
}
|
|
2144
2177
|
return n;
|
|
2145
2178
|
};
|
|
2146
|
-
const generatedKW =
|
|
2147
|
-
const exportKW =
|
|
2148
|
-
const soc =
|
|
2149
|
-
const chargeKW =
|
|
2150
|
-
const dischargeKW =
|
|
2151
|
-
const minKW =
|
|
2179
|
+
const generatedKW = num2(r?.generatedKW, "generatedKW", 0);
|
|
2180
|
+
const exportKW = num2(r?.exportKW, "exportKW");
|
|
2181
|
+
const soc = num2(r?.soc, "soc", 0, 100);
|
|
2182
|
+
const chargeKW = num2(r?.chargeKW, "chargeKW", 0);
|
|
2183
|
+
const dischargeKW = num2(r?.dischargeKW, "dischargeKW", 0);
|
|
2184
|
+
const minKW = num2(r?.minKW, "minKW", 0);
|
|
2152
2185
|
let curtailable;
|
|
2153
2186
|
if (r?.curtailable !== void 0 && r?.curtailable !== null) {
|
|
2154
2187
|
if (typeof r.curtailable !== "boolean") errors.push(`curtailable \uAC00 \uCC38/\uAC70\uC9D3\uC774 \uC544\uB2C8\uB2E4: ${JSON.stringify(r.curtailable)}`);
|
|
2155
2188
|
else curtailable = r.curtailable;
|
|
2156
2189
|
}
|
|
2157
|
-
const dcVoltage =
|
|
2158
|
-
const dcCurrent =
|
|
2190
|
+
const dcVoltage = num2(r?.dcVoltage, "dcVoltage", 0);
|
|
2191
|
+
const dcCurrent = num2(r?.dcCurrent, "dcCurrent", 0);
|
|
2159
2192
|
const phases = (raw, name) => {
|
|
2160
2193
|
if (raw === void 0 || raw === null) return void 0;
|
|
2161
2194
|
if (!Array.isArray(raw)) {
|
|
@@ -3371,7 +3404,7 @@ var SPECS = {
|
|
|
3371
3404
|
identity: "moverId",
|
|
3372
3405
|
// vocabulary-guard: allow 저널 와이어 필드
|
|
3373
3406
|
/* 누적 카운터가 없으면 OEE 가 양품률을 못 센다 — 판정 하나만으로는 비율이 나오지 않는다. */
|
|
3374
|
-
required: ["moverId", "good", "goodCount", "
|
|
3407
|
+
required: ["moverId", "good", "goodCount", "nonconformingCount"],
|
|
3375
3408
|
// vocabulary-guard: allow
|
|
3376
3409
|
/*
|
|
3377
3410
|
* `taskId` 는 **귀속**이다 — 이 산출이 어느 작업의 것인가. 수량의 뜻은 바뀌지 않는다(설비 누적).
|
|
@@ -3382,7 +3415,7 @@ var SPECS = {
|
|
|
3382
3415
|
* 정체를 `taskId` 로 바꾸지 않는 이유는 OEE 가 설비 단위여서다. 정체를 옮기면 설비 누적을 셀 수
|
|
3383
3416
|
* 없어진다.
|
|
3384
3417
|
*/
|
|
3385
|
-
fields: { moverId: "string", good: "boolean", goodCount: "number",
|
|
3418
|
+
fields: { moverId: "string", good: "boolean", goodCount: "number", nonconformingCount: "number", taskId: "string", recordTime: "string" }
|
|
3386
3419
|
// vocabulary-guard: allow
|
|
3387
3420
|
},
|
|
3388
3421
|
/*
|
|
@@ -3614,7 +3647,7 @@ function ingestOperationalRecords(records, opts) {
|
|
|
3614
3647
|
errors.push(`${kind}.progress \uB294 0~1 \uBE44\uC728\uC774\uB2E4: ${n}`);
|
|
3615
3648
|
break;
|
|
3616
3649
|
}
|
|
3617
|
-
if ((name === "requested" || name === "fulfilled" || name === "goodCount" || name === "
|
|
3650
|
+
if ((name === "requested" || name === "fulfilled" || name === "goodCount" || name === "nonconformingCount") && n < 0) {
|
|
3618
3651
|
errors.push(`${kind}.${name} \uAC00 \uC74C\uC218\uB2E4: ${n}`);
|
|
3619
3652
|
break;
|
|
3620
3653
|
}
|
|
@@ -3790,7 +3823,7 @@ function validateScenario(def) {
|
|
|
3790
3823
|
}
|
|
3791
3824
|
|
|
3792
3825
|
// src/vocabulary.ts
|
|
3793
|
-
var RETIRED_VOCABULARY = ["mover", "Mover", "MOVER", "node", "Node", "NODE", "BoardDef", "loadBoard"];
|
|
3826
|
+
var RETIRED_VOCABULARY = ["mover", "Mover", "MOVER", "node", "Node", "NODE", "BoardDef", "loadBoard", "scrapCount"];
|
|
3794
3827
|
var VOCABULARY_EXCEPTIONS = [
|
|
3795
3828
|
/* ── 저널 와이어 필드 — append-only 역사이므로 이름을 바꾸지 않는다 ───────
|
|
3796
3829
|
* 같은 사실이 시점에 따라 다른 키로 들어가면 낡은 이름보다 나쁘다. 개명은 이벤트 스키마
|
|
@@ -3894,7 +3927,7 @@ function computeOee(c, nowMs) {
|
|
|
3894
3927
|
if (planned <= 0) missing.push("planned-busy-time");
|
|
3895
3928
|
const delayMs = Math.max(0, planned - c.runMs - c.setupMs - c.downMs);
|
|
3896
3929
|
const availability = planned > 0 ? Math.min(1, c.runMs / planned) : void 0;
|
|
3897
|
-
const produced = c.goodCount + c.
|
|
3930
|
+
const produced = c.goodCount + c.nonconformingCount;
|
|
3898
3931
|
if (produced <= 0) missing.push("produced-quantity");
|
|
3899
3932
|
const quality = produced > 0 ? c.goodCount / produced : void 0;
|
|
3900
3933
|
if (c.plannedRunTimePerItemMs == null) missing.push("planned-run-time-per-item");
|
|
@@ -3912,7 +3945,7 @@ function computeOee(c, nowMs) {
|
|
|
3912
3945
|
downMs: c.downMs,
|
|
3913
3946
|
delayMs,
|
|
3914
3947
|
goodCount: c.goodCount,
|
|
3915
|
-
|
|
3948
|
+
nonconformingCount: c.nonconformingCount
|
|
3916
3949
|
};
|
|
3917
3950
|
}
|
|
3918
3951
|
|
|
@@ -4216,6 +4249,7 @@ function commandSpecGaps(specs, command) {
|
|
|
4216
4249
|
TWIN_AXES,
|
|
4217
4250
|
TWIN_PROPERTIES,
|
|
4218
4251
|
TWIN_RELATIONS,
|
|
4252
|
+
USE_UOM,
|
|
4219
4253
|
UTC_OFFSET,
|
|
4220
4254
|
VOCABULARY_EXCEPTIONS,
|
|
4221
4255
|
VOCABULARY_TYPE,
|
|
@@ -4257,6 +4291,7 @@ function commandSpecGaps(specs, command) {
|
|
|
4257
4291
|
dueStatusOf,
|
|
4258
4292
|
effectivityAt,
|
|
4259
4293
|
electricalUpstreamOf,
|
|
4294
|
+
equipmentClassMembership,
|
|
4260
4295
|
erpCapabilityGaps,
|
|
4261
4296
|
expiryFromAttributes,
|
|
4262
4297
|
gdtiUri,
|
|
@@ -4332,6 +4367,7 @@ function commandSpecGaps(specs, command) {
|
|
|
4332
4367
|
procedureViolations,
|
|
4333
4368
|
propertiesOf,
|
|
4334
4369
|
quantityIn,
|
|
4370
|
+
ratedUsageOf,
|
|
4335
4371
|
readBoardAssets,
|
|
4336
4372
|
readBoardEquipment,
|
|
4337
4373
|
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.3",
|
|
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",
|