@operato/twin-kernel 0.8.4 → 0.9.0

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,4 +1,4 @@
1
- import type { TestResult, ISOTime, MaterialQuantity, WorkCalendarEntry, EffectivePeriod, Effectivity, OffCalendarReason, ResourceProperty, ResourceClassDef, MaterialDefinition, Attention, TwinModelDef, CanonicalEnvelope, Command, CommandAck, EventHandler, EquipmentMotion, OeeMetrics, AssetState, GeneratorSpec, InterventionOutcome, OrderState, PersonState, ScenarioControl, ScenarioOverride, StateSnapshot, TwinKernel, Unsubscribe, LocationState, ItemState, EquipmentState, OrderStatusDelta, TaskState, TaskStatus, StructureShift, IdentityGroundingView, IdentityDeclaration, TestSpecificationCriterion, LocationObservation } from '@operato/ops-contract';
1
+ import type { TestResult, ISOTime, MaterialQuantity, WorkCalendarEntry, EffectivePeriod, Effectivity, OffCalendarReason, ResourceProperty, ResourceClassDef, MaterialDefinition, Attention, TwinModelDef, CanonicalEnvelope, Command, CommandAck, EventHandler, EquipmentMotion, AssetState, GeneratorSpec, InterventionOutcome, OrderState, PersonState, ScenarioControl, ScenarioOverride, StateSnapshot, TwinKernel, Unsubscribe, LocationState, ItemState, EquipmentState, OrderStatusDelta, TaskState, TaskStatus, StructureShift, IdentityGroundingView, IdentityDeclaration, TestSpecificationCriterion, LocationObservation } from '@operato/ops-contract';
2
2
  import type { VocabularyElement } from '@operato/ops-contract';
3
3
  import type { ReducerCheckpoint } from './observed-reducer.ts';
4
4
  import type { EpcisEvent, BizTransactionElement } from '@operato/ops-contract';
@@ -411,22 +411,6 @@ nowIso?: ISOTime,
411
411
  * 부르는 쪽이 모델에서 뽑아 넘긴다(이 함수는 순수하게 남는다 — 모델을 읽는 규칙이 두 곳이 되지 않게).
412
412
  */
413
413
  thresholds?: AttentionThresholds): Attention[];
414
- /** OEE 계측 카운터 — sim 은 tick 으로 누적, live 는 실 텔레메트리 또는 이벤트 누적기가 채운다(face2-inbound-live §1.1). */
415
- export interface OeeCounters {
416
- runMs: number;
417
- setupMs: number;
418
- downMs: number;
419
- goodCount: number;
420
- scrapCount: number;
421
- holdMs?: number;
422
- metricsSinceMs?: number;
423
- }
424
- /**
425
- * OEE 공식(순수) — 누적 카운터 + 현재 시각 → OEE. sim(oeeOf)과 live 가 **공유하는 계산 층**(face2-inbound-live §1.1).
426
- * ⚠ attentions 와 달리: 입력(카운터)이 관측 State·이벤트에 없다 → live 는 카운터를 텔레메트리(정확) 또는
427
- * 이벤트 누적기(근사)로 별도 공급해야 한다. 공식만 공유되고 입력원은 live 데이터-생산 결정.
428
- */
429
- export declare function computeOee(c: OeeCounters, nowMs: number): OeeMetrics;
430
414
  /**
431
415
  * 물품 저장소 — **맵과 「자리별 색인」을 함께 든다.**
432
416
  *
@@ -9,7 +9,7 @@
9
9
  * 통합 타입은 도메인 필드를 옵셔널로 넓혀(FlowItem.gtin?, FlowOrder.shipmentEpc? 등) 두 도메인을 담는다.
10
10
  * (roadmap Phase5 발견 → 추출. [[project_flow_single_base_vision]] FlowLocation 단일 base 방향과 정합.)
11
11
  */
12
- import { OP_EVENT, CMD, locationStatusOf, readBoardEquipment, readBoardLocations, readBoardAssets, classClosure, capabilityOf, requiredTestsFor, priorityRank, dueStatusOf, isOrderTerminal, effectivityAt, offCalendarAt, offCalendarReasonAt, minuteOfDayAt, activeShiftAt, subLotIdOf, itemKeyOf, identityGroundingOf, outsideLimit } from '@operato/ops-contract';
12
+ import { OP_EVENT, CMD, locationStatusOf, readBoardEquipment, readBoardLocations, readBoardAssets, classClosure, capabilityOf, requiredTestsFor, priorityRank, dueStatusOf, isOrderTerminal, effectivityAt, offCalendarAt, offCalendarReasonAt, minuteOfDayAt, activeShiftAt, subLotIdOf, itemKeyOf, identityGroundingOf, outsideLimit, computeOee } from '@operato/ops-contract';
13
13
  import { ObservedReducer } from "./observed-reducer.js";
14
14
  /* 주체를 정하는 규칙은 유입 문과 한 벌이다 — 두 곳에 적으면 한쪽만 고쳐진다. */
15
15
  import { resolveSubject } from '@operato/ops-contract';
@@ -332,32 +332,6 @@ thresholds) {
332
332
  a.state = 'acknowledged';
333
333
  return out;
334
334
  }
335
- /**
336
- * OEE 공식(순수) — 누적 카운터 + 현재 시각 → OEE. sim(oeeOf)과 live 가 **공유하는 계산 층**(face2-inbound-live §1.1).
337
- * ⚠ attentions 와 달리: 입력(카운터)이 관측 State·이벤트에 없다 → live 는 카운터를 텔레메트리(정확) 또는
338
- * 이벤트 누적기(근사)로 별도 공급해야 한다. 공식만 공유되고 입력원은 live 데이터-생산 결정.
339
- */
340
- export function computeOee(c, nowMs) {
341
- /*
342
- * **계측 창을 모르면 잰 시간이 없다** — `?? 0` 으로 메우면 "1970년부터 재고 있었다" 가 된다.
343
- *
344
- * 그 결과가 화면까지 갔다: 갓 뜬 트윈의 절단기가 `idleMs 1767226603000`(약 56,000년) 유휴로 섰다.
345
- * 시각을 소요시간 자리에 앉힌 것이고, 같은 값이 availability·overall 의 분모라 **성과 숫자 전체가
346
- * 조용히 거짓**이 된다(가동률 100%, 성능 0%).
347
- *
348
- * 모르면 0 이 아니라 **없음**이다: 잰 구간이 없으면 유휴도 없다(아직 아무것도 재지 않았다).
349
- */
350
- const planned = c.metricsSinceMs == null ? 0 : Math.max(0, nowMs - c.metricsSinceMs - (c.holdMs ?? 0));
351
- const uptime = Math.max(0, planned - c.setupMs - c.downMs); // 가용시간(셋업·고장 제외)
352
- const availability = planned > 0 ? uptime / planned : 1;
353
- const performance = uptime > 0 ? Math.min(1, c.runMs / uptime) : (c.runMs > 0 ? 1 : 0);
354
- const totalQ = c.goodCount + c.scrapCount;
355
- const quality = totalQ > 0 ? c.goodCount / totalQ : 1;
356
- return {
357
- availability, performance, quality, overall: availability * performance * quality,
358
- runMs: c.runMs, setupMs: c.setupMs, downMs: c.downMs, idleMs: Math.max(0, uptime - c.runMs), goodCount: c.goodCount, scrapCount: c.scrapCount
359
- };
360
- }
361
335
  /**
362
336
  * 에너지 능력이 선언한 상태 필드를 스냅샷에 싣는다 — **온 것만.**
363
337
  *
@@ -1075,7 +1049,7 @@ export class FlowEngine {
1075
1049
  * 한계가 있다 — busy 를 가동으로 근사하고 **셋업을 분리하지 못해 0** 으로 둔다. 여기서는 받은
1076
1050
  * 값을 그대로 이어받고, 없으면 0 에서 시작한다(꾸미지 않는다).
1077
1051
  */
1078
- /* `idleMs` 까지 읽는다 — 그것이 있어야 **잰 구간**을 되세울 수 있다(계획 시간 = 가동+준비+고장+유휴). */
1052
+ /* `delayMs` 까지 읽는다 — 그것이 있어야 **잰 구간**을 되세울 수 있다(계획 조업 = 생산+준비+고장+대기). */
1079
1053
  const oee = m.oee;
1080
1054
  /* **마스터가 말한 것을 관측이 지우지 않는다.** 예전에는 이 자리에서 통째로 새 객체를 만들어
1081
1055
  * 교대 캘린더·속성·유효 기간·계획정지를 **전부 버렸다.** 그 결과 라이브(관측) 커널은
@@ -1105,7 +1079,7 @@ export class FlowEngine {
1105
1079
  * 뒤로 물려 창을 다시 세운다. 지표가 없으면 창도 없다(그때는 지금부터 새로 잰다).
1106
1080
  */
1107
1081
  ...(oee
1108
- ? { metricsSinceMs: this.clockMs - ((oee.runMs ?? 0) + (oee.setupMs ?? 0) + (oee.downMs ?? 0) + (oee.idleMs ?? 0)) }
1082
+ ? { metricsSinceMs: this.clockMs - ((oee.runMs ?? 0) + (oee.setupMs ?? 0) + (oee.downMs ?? 0) + (oee.delayMs ?? 0)) }
1109
1083
  : {}),
1110
1084
  /* 계획 정지를 이어받는다 — 잃으면 씨앗이 **정비 중인 설비를 가용으로 놓고** 미래를 시뮬레이션한다
1111
1085
  (예측이 낙관 쪽으로 치우친다). 씨앗 왕복 대조가 이것을 잡았다. */
@@ -38,7 +38,6 @@ __export(index_exports, {
38
38
  YmsKernel: () => YmsKernel,
39
39
  attributeEnergy: () => attributeEnergy,
40
40
  compareStates: () => compareStates,
41
- computeOee: () => computeOee,
42
41
  constantDuration: () => constantDuration,
43
42
  counterfactualAt: () => counterfactualAt,
44
43
  demandWindowStart: () => demandWindowStart,
@@ -63,7 +62,7 @@ __export(index_exports, {
63
62
  });
64
63
  module.exports = __toCommonJS(index_exports);
65
64
 
66
- // node_modules/@operato/ops-contract/dist/capability.js
65
+ // ../ops-contract/dist/capability.js
67
66
  var CAPABILITIES = {
68
67
  operable: {
69
68
  key: "operable",
@@ -198,7 +197,7 @@ function stateFieldsOf(caps) {
198
197
  return [...out];
199
198
  }
200
199
 
201
- // node_modules/@operato/ops-contract/dist/contract.js
200
+ // ../ops-contract/dist/contract.js
202
201
  var ORDER_TERMINAL_STATUS = ["completed", "cancelled"];
203
202
  function isOrderTerminal(o) {
204
203
  const status = String(o?.status ?? "").trim().toLowerCase();
@@ -582,6 +581,15 @@ var OP_EVENT = {
582
581
  * 이 채널이 없으면 시험 결과는 **상태에만 있는 축**이 된다 — 재기동에서 사라지고, 폴드가 되살릴 수
583
582
  * 없고, 미러가 이어받지 못한다(§상태 ⊆ 이벤트).
584
583
  */
584
+ /**
585
+ * **마감된 설비 상태 구간** — 이 설비가 이 구간 동안 이 상태였다.
586
+ *
587
+ * 시점의 전이(`equipment.status`)와 다른 사실이다. 전이는 설비가 내고, 이것은 **사람이 나중에
588
+ * 적는다.** 자재 대기처럼 신호를 내지 않는 정지는 이 길로만 들어온다.
589
+ *
590
+ * ISO 22400-2 의 가동률 계산이 이 구간들을 읽는다.
591
+ */
592
+ equipmentPeriod: "equipment.state.period",
585
593
  test: "test.result",
586
594
  /**
587
595
  * **부적합 처분** — 재고 판정을 받은 것을 어떻게 하기로 정했나(재작업 · 특채 · 폐기 · 반품).
@@ -805,7 +813,7 @@ function gs1Namespaces(prefix) {
805
813
  return [`urn:epc:idpat:sgtin:${prefix}.`, `urn:epc:class:lgtin:${prefix}.`, `urn:epc:id:sgtin:${prefix}.`];
806
814
  }
807
815
 
808
- // node_modules/@operato/ops-contract/dist/capacity.js
816
+ // ../ops-contract/dist/capacity.js
809
817
  function isoDurationHours(iso2) {
810
818
  if (!iso2)
811
819
  return 0;
@@ -913,7 +921,7 @@ function analyzeCapacity(input) {
913
921
  };
914
922
  }
915
923
 
916
- // node_modules/@operato/ops-contract/dist/wms-profile.js
924
+ // ../ops-contract/dist/wms-profile.js
917
925
  var BIZSTEP = {
918
926
  receiving: "urn:epcglobal:cbv:bizstep:receiving",
919
927
  storing: "urn:epcglobal:cbv:bizstep:storing",
@@ -943,7 +951,7 @@ var WMS_TYPES = [
943
951
  { key: "packer", role: "equipment", label: "twin.type.packer", standardClass: { epcis: "object", isa95: "Equipment" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] }
944
952
  ];
945
953
 
946
- // node_modules/@operato/ops-contract/dist/yms-profile.js
954
+ // ../ops-contract/dist/yms-profile.js
947
955
  var YARD_BIZSTEP = {
948
956
  arriving: "urn:epcglobal:cbv:bizstep:arriving",
949
957
  // 게이트-인
@@ -969,7 +977,7 @@ var YMS_TYPES = [
969
977
  { key: "hostler", role: "equipment", label: "twin.type.hostler", standardClass: { epcis: "object", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["mobile", "operable"] }
970
978
  ];
971
979
 
972
- // node_modules/@operato/ops-contract/dist/mes-profile.js
980
+ // ../ops-contract/dist/mes-profile.js
973
981
  var MES_BIZSTEP = {
974
982
  receiving: "urn:epcglobal:cbv:bizstep:receiving",
975
983
  // 원자재 수령
@@ -1007,7 +1015,7 @@ var MES_TYPES = [
1007
1015
  { key: "assembler", role: "equipment", label: "twin.type.assembler", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] }
1008
1016
  ];
1009
1017
 
1010
- // node_modules/@operato/ops-contract/dist/ems-profile.js
1018
+ // ../ops-contract/dist/ems-profile.js
1011
1019
  var EMS_LOCATION_TYPES = ["incoming", "feeder", "submeter-zone"];
1012
1020
  var METER_DIRECTION = ["import", "export", "bidirectional"];
1013
1021
  var EMS_PROPERTY = {
@@ -1328,7 +1336,7 @@ function isElectricalLocationType(type) {
1328
1336
  return EMS_LOCATION_TYPES.includes(type);
1329
1337
  }
1330
1338
 
1331
- // node_modules/@operato/ops-contract/dist/domain-catalog.js
1339
+ // ../ops-contract/dist/domain-catalog.js
1332
1340
  var locationKeys = (types) => types.filter((t) => t.role === "location").map((t) => t.key);
1333
1341
  var DOMAIN_CATALOG = {
1334
1342
  // label 은 언어 중립 i18n 키(twin.system.<code>) — 사람 언어는 표현계층이 렌더(L2).
@@ -1338,7 +1346,7 @@ var DOMAIN_CATALOG = {
1338
1346
  ems: { system: "ems", label: "twin.system.ems", types: EMS_TYPES, locationTypes: locationKeys(EMS_TYPES) }
1339
1347
  };
1340
1348
 
1341
- // node_modules/@operato/ops-contract/dist/domain-definition.js
1349
+ // ../ops-contract/dist/domain-definition.js
1342
1350
  var OP_PARAM = {
1343
1351
  /** 양품률(0..1, 무차원). 없으면 커널 기본값 — 기본값을 쓴 사실은 `specCoverage()` 가 밝힌다. */
1344
1352
  yield: "yield",
@@ -1347,7 +1355,7 @@ var OP_PARAM = {
1347
1355
  setupDuration: "setupDuration"
1348
1356
  };
1349
1357
 
1350
- // node_modules/@operato/ops-contract/dist/energy-ingest.js
1358
+ // ../ops-contract/dist/energy-ingest.js
1351
1359
  function resolveSubject(kind, localId, opts) {
1352
1360
  const declared = opts.identityOf?.(kind, localId);
1353
1361
  if (declared)
@@ -1358,7 +1366,7 @@ function periodFactId(tenantId, kind, subject, from, to) {
1358
1366
  return [tenantId, kind, subject, from, to].join("|");
1359
1367
  }
1360
1368
 
1361
- // node_modules/@operato/ops-contract/dist/epcis.js
1369
+ // ../ops-contract/dist/epcis.js
1362
1370
  var EPCIS_CONTEXT = "https://ref.gs1.org/standards/epcis/2.0.0/epcis-context.jsonld";
1363
1371
  var UTC_OFFSET = "+00:00";
1364
1372
  var DISP = {
@@ -1617,7 +1625,7 @@ function transformationEvent(p) {
1617
1625
  return e;
1618
1626
  }
1619
1627
 
1620
- // node_modules/@operato/ops-contract/dist/iso-duration.js
1628
+ // ../ops-contract/dist/iso-duration.js
1621
1629
  var RE = /^(-)?P(?:(\d+(?:\.\d+)?)W)?(?:(\d+(?:\.\d+)?)D)?(?:T(?:(\d+(?:\.\d+)?)H)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)S)?)?$/;
1622
1630
  function parseIsoDuration(text) {
1623
1631
  if (typeof text !== "string")
@@ -1643,7 +1651,7 @@ function parseIsoDuration(text) {
1643
1651
  return sign ? -ms2 : ms2;
1644
1652
  }
1645
1653
 
1646
- // node_modules/@operato/ops-contract/dist/master-data.js
1654
+ // ../ops-contract/dist/master-data.js
1647
1655
  function expiryFromAttributes(attrs) {
1648
1656
  return readEpochMs(attrs?.[ILMD_ATTR.expiry]);
1649
1657
  }
@@ -1660,9 +1668,9 @@ function readEpochMs(raw) {
1660
1668
  return Number.isNaN(at) ? void 0 : at;
1661
1669
  }
1662
1670
 
1663
- // node_modules/@operato/ops-contract/dist/operational-ingest.js
1671
+ // ../ops-contract/dist/operational-ingest.js
1664
1672
  var TASK_STATUS = ["created", "assigned", "in-progress", "completed"];
1665
- var EQUIPMENT_STATUS = ["idle", "busy", "down"];
1673
+ var EQUIPMENT_STATUS = ["idle", "busy", "down", "setup", "planned-stop"];
1666
1674
  var PERSON_STATUS = ["idle", "busy"];
1667
1675
  var ASSET_STATUS = ["idle", "in-use"];
1668
1676
  var SPECS = {
@@ -1720,6 +1728,8 @@ var SPECS = {
1720
1728
  effectiveStart: "string",
1721
1729
  effectiveEnd: "string",
1722
1730
  recordTime: "string",
1731
+ /* 왜 이 상태가 됐나 — 설비가 알려 주면 싣는다. 사람이 나중에 정하는 사유는 구간 사실에 적는다. */
1732
+ reasonCode: "string",
1723
1733
  /* 이동 구간 — 실 시스템도 줄 수 있는 사실이다(AGV·RTLS 가 출발·도착·소요를 낸다). 안쪽 필드까지
1724
1734
  재검사하지는 않는다: 그 모양은 `EquipmentMotion` 계약이고, 여기서 두 번 지키면 두 벌이 된다. */
1725
1735
  motion: "object"
@@ -1809,6 +1819,49 @@ var SPECS = {
1809
1819
  * `propertyMeasurements` 안쪽은 재검사하지 않는다 — 그 모양은 `PropertyMeasurement` 계약이고,
1810
1820
  * 여기서 두 번 지키면 두 벌이 된다(설비 `motion` 과 같은 규율).
1811
1821
  */
1822
+ /*
1823
+ * **마감된 설비 상태 구간** — 이 설비가 이 구간 동안 이 상태였다.
1824
+ *
1825
+ * ── 전이만으로는 안 되는 이유 (2026-08-30) ───────────────────────────────
1826
+ * 세 가지다. 셋째가 결정적이다.
1827
+ *
1828
+ * 사유가 붙을 자리가 없다 기계가 서는 순간에는 왜 섰는지 아무도 모른다. 작업자가 라인이 다시
1829
+ * 돈 뒤에 적는다. 그 사이에 전이가 더 있으면 어느 전이에 붙일지 정할 수 없다
1830
+ * 계획·비계획을 나중에 정한다 ISO 22400 은 계획정지를 계획 조업 시간에서 빼고 고장은 빼지 않는다.
1831
+ * 그 분류는 전이가 일어난 순간에 모른다
1832
+ * 전이를 내지 않는 정지가 많다 자재 대기 · 앞 공정 대기 · 작업자 부재. 기계는 전원이 켜진 채 `idle`
1833
+ * 이고 신호가 하나도 안 나온다. OEE 에서 가장 크게 깎이는 것이 보통 이 시간이다
1834
+ *
1835
+ * 마지막이 감시 시스템과 MES 가 갈리는 자리이기도 하다. 설비가 말하는 것만 모으면 감시이고,
1836
+ * **설비가 말하지 못하는 것을 사람이 적어 넣는 자리**가 MES 다.
1837
+ *
1838
+ * ── 이름이 `downtime` 이 아닌 이유 ────────────────────────────────────────
1839
+ * 이 구간은 `setup` 과 `planned-stop` 도 나른다. 그 둘은 정지가 아니다. 담는 것은 **상태 구간**이고,
1840
+ * 어느 상태인지는 `status` 가 말한다.
1841
+ *
1842
+ * ── 되돌아가 적는 사실이다 ───────────────────────────────────────────────
1843
+ * 봉투의 사건 시각은 **구간의 끝**(그때 성립한다)이고, 적은 시각은 `recordTime` 이다. 둘이 갈려 있어야
1844
+ * 「언제 일어났나」와 「언제 알았나」를 구별할 수 있다.
1845
+ */
1846
+ "equipment-period": {
1847
+ eventType: OP_EVENT.equipmentPeriod,
1848
+ identity: "moverId",
1849
+ // vocabulary-guard: allow 저널 와이어 필드 — 전이와 같은 이름을 쓴다
1850
+ required: ["moverId", "status", "from", "to"],
1851
+ // vocabulary-guard: allow 위와 같은 이유
1852
+ fields: {
1853
+ moverId: "string",
1854
+ status: "string",
1855
+ from: "string",
1856
+ to: "string",
1857
+ // vocabulary-guard: allow
1858
+ /* 사람이 정한 사유 — 없을 수 있다(적지 않은 것과 사유가 없는 것은 다르므로 지어내지 않는다). */
1859
+ reasonCode: "string",
1860
+ decidedBy: "string",
1861
+ recordTime: "string"
1862
+ },
1863
+ enums: { status: EQUIPMENT_STATUS }
1864
+ },
1812
1865
  /*
1813
1866
  * **부적합 처분** — 재고 판정을 받은 것을 어떻게 하기로 정했나.
1814
1867
  *
@@ -1901,7 +1954,7 @@ var SPECS = {
1901
1954
  }
1902
1955
  };
1903
1956
 
1904
- // node_modules/@operato/ops-contract/dist/operations-capability.js
1957
+ // ../ops-contract/dist/operations-capability.js
1905
1958
  function workingHoursBetween(calendar, fromMs, toMs, utcOffsetMinutes) {
1906
1959
  if (!(toMs > fromMs))
1907
1960
  return 0;
@@ -1971,6 +2024,39 @@ function operationsCapabilityOf(input) {
1971
2024
  };
1972
2025
  }
1973
2026
 
2027
+ // ../ops-contract/dist/oee.js
2028
+ function computeOee(c, nowMs) {
2029
+ const missing = [];
2030
+ const planned = c.metricsSinceMs == null ? 0 : Math.max(0, nowMs - c.metricsSinceMs - (c.holdMs ?? 0));
2031
+ if (planned <= 0)
2032
+ missing.push("planned-busy-time");
2033
+ const delayMs = Math.max(0, planned - c.runMs - c.setupMs - c.downMs);
2034
+ const availability = planned > 0 ? Math.min(1, c.runMs / planned) : void 0;
2035
+ const produced = c.goodCount + c.scrapCount;
2036
+ if (produced <= 0)
2037
+ missing.push("produced-quantity");
2038
+ const quality = produced > 0 ? c.goodCount / produced : void 0;
2039
+ if (c.plannedRunTimePerItemMs == null)
2040
+ missing.push("planned-run-time-per-item");
2041
+ else if (c.runMs <= 0)
2042
+ missing.push("actual-production-time");
2043
+ const performance = c.plannedRunTimePerItemMs != null && c.runMs > 0 ? Math.min(1, c.plannedRunTimePerItemMs * produced / c.runMs) : void 0;
2044
+ const overall = availability != null && performance != null && quality != null ? availability * performance * quality : void 0;
2045
+ return {
2046
+ ...availability != null ? { availability } : {},
2047
+ ...performance != null ? { performance } : {},
2048
+ ...quality != null ? { quality } : {},
2049
+ ...overall != null ? { overall } : {},
2050
+ ...missing.length ? { missing } : {},
2051
+ runMs: c.runMs,
2052
+ setupMs: c.setupMs,
2053
+ downMs: c.downMs,
2054
+ delayMs,
2055
+ goodCount: c.goodCount,
2056
+ scrapCount: c.scrapCount
2057
+ };
2058
+ }
2059
+
1974
2060
  // src/divergence.ts
1975
2061
  function diffBy(predicted, actual, idOf, valOf) {
1976
2062
  const p = new Map(predicted.map((e) => [idOf(e), valOf(e)]));
@@ -3351,26 +3437,6 @@ function deriveAttentions(view, acked, nowIso, thresholds) {
3351
3437
  }
3352
3438
  return out;
3353
3439
  }
3354
- function computeOee(c, nowMs) {
3355
- const planned = c.metricsSinceMs == null ? 0 : Math.max(0, nowMs - c.metricsSinceMs - (c.holdMs ?? 0));
3356
- const uptime = Math.max(0, planned - c.setupMs - c.downMs);
3357
- const availability = planned > 0 ? uptime / planned : 1;
3358
- const performance = uptime > 0 ? Math.min(1, c.runMs / uptime) : c.runMs > 0 ? 1 : 0;
3359
- const totalQ = c.goodCount + c.scrapCount;
3360
- const quality = totalQ > 0 ? c.goodCount / totalQ : 1;
3361
- return {
3362
- availability,
3363
- performance,
3364
- quality,
3365
- overall: availability * performance * quality,
3366
- runMs: c.runMs,
3367
- setupMs: c.setupMs,
3368
- downMs: c.downMs,
3369
- idleMs: Math.max(0, uptime - c.runMs),
3370
- goodCount: c.goodCount,
3371
- scrapCount: c.scrapCount
3372
- };
3373
- }
3374
3440
  var ENERGY_STATE_FIELDS = [
3375
3441
  ...stateFieldsOf(["metered", "curtailable", "energyGenerating", "energyStoring", "switching"]),
3376
3442
  "measuredAt"
@@ -3995,7 +4061,7 @@ var FlowEngine = class {
3995
4061
  * 창은 지표 자신이 말해 준다 — 계획 시간은 가동·준비·고장·유휴의 합이다. 그래서 그 합만큼
3996
4062
  * 뒤로 물려 창을 다시 세운다. 지표가 없으면 창도 없다(그때는 지금부터 새로 잰다).
3997
4063
  */
3998
- ...oee ? { metricsSinceMs: this.clockMs - ((oee.runMs ?? 0) + (oee.setupMs ?? 0) + (oee.downMs ?? 0) + (oee.idleMs ?? 0)) } : {},
4064
+ ...oee ? { metricsSinceMs: this.clockMs - ((oee.runMs ?? 0) + (oee.setupMs ?? 0) + (oee.downMs ?? 0) + (oee.delayMs ?? 0)) } : {},
3999
4065
  /* 계획 정지를 이어받는다 — 잃으면 씨앗이 **정비 중인 설비를 가용으로 놓고** 미래를 시뮬레이션한다
4000
4066
  (예측이 낙관 쪽으로 치우친다). 씨앗 왕복 대조가 이것을 잡았다. */
4001
4067
  ...m.held ? { held: true } : {},
@@ -9420,7 +9486,6 @@ function electricityCost(input) {
9420
9486
  YmsKernel,
9421
9487
  attributeEnergy,
9422
9488
  compareStates,
9423
- computeOee,
9424
9489
  constantDuration,
9425
9490
  counterfactualAt,
9426
9491
  demandWindowStart,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/twin-kernel",
3
- "version": "0.8.4",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "description": "Twin Domain Kernel — framework-agnostic, zero-dep (domain + sim + 3-channel contract). WMS/YMS/MES, EPCIS 2.0 · ISA-95.",
6
6
  "publishConfig": {
@@ -28,6 +28,6 @@
28
28
  "test": "node --test test/*.test.ts"
29
29
  },
30
30
  "dependencies": {
31
- "@operato/ops-contract": "^0.4.0"
31
+ "@operato/ops-contract": "^0.5.0"
32
32
  }
33
33
  }