@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.
@@ -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
- scrapCount: number;
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
- /** 품질 산출 델타 — recordOutput(양품/불량) 시 방출. goodCount/scrapCount 는 설비 누적값. */
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
- scrapCount: number;
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
- * 설비(설비). mtbfMs/mttrMs 지정 시 확률적 고장 모델 참여(OEE Availability 손실). 미지정=고장 없음.
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
- scrapCount: number;
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.scrapCount;
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
- scrapCount: c.scrapCount
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', 'scrapCount'], // vocabulary-guard: allow
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', scrapCount: 'number', taskId: 'string', recordTime: 'string' } // vocabulary-guard: allow
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 === 'scrapCount') && n < 0) {
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
+ }
@@ -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
  *
@@ -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
  *
@@ -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 num = (v, name) => {
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 = num(record?.kW, "kW");
2083
- const kWh = num(record?.kWh, "kWh");
2084
- const powerFactor = num(record?.powerFactor, "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 num = (v, name, min, max) => {
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 = num(r?.generatedKW, "generatedKW", 0);
2147
- const exportKW = num(r?.exportKW, "exportKW");
2148
- const soc = num(r?.soc, "soc", 0, 100);
2149
- const chargeKW = num(r?.chargeKW, "chargeKW", 0);
2150
- const dischargeKW = num(r?.dischargeKW, "dischargeKW", 0);
2151
- const minKW = num(r?.minKW, "minKW", 0);
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 = num(r?.dcVoltage, "dcVoltage", 0);
2158
- const dcCurrent = num(r?.dcCurrent, "dcCurrent", 0);
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", "scrapCount"],
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", scrapCount: "number", taskId: "string", recordTime: "string" }
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 === "scrapCount") && n < 0) {
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.scrapCount;
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
- scrapCount: c.scrapCount
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.1",
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",