@operato/twin-kernel 0.8.3 → 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();
@@ -218,6 +217,32 @@ function locationStatusOf(n) {
218
217
  const r = (n.occupancy ?? 0) / cap;
219
218
  return r >= 1 ? "full" : r >= LOCATION_SATURATION_NEAR ? "near-full" : "available";
220
219
  }
220
+ var DISPOSITION_DECISION = [
221
+ /** 원 규격으로 되돌린다 — 공정에 다시 들어가고, 산출물은 원 품목이다. */
222
+ "rework",
223
+ /**
224
+ * 쓸 수 있게 만들되 **원 규격은 아니다** — 공정에 다시 들어가지만 산출물이 원 품목이 아니다.
225
+ *
226
+ * `rework` 와 나눈 이유는 이름이 아니라 **효과**다. 되돌린 것과 고쳐 쓴 것은 산출물의 정체성이
227
+ * 다르고, 그것을 한 낱말로 묶으면 계보가 「원 품목을 만들었다」고 거짓을 말한다.
228
+ */
229
+ "repair",
230
+ /** 그대로 쓴다 — 공정도 정체성도 바뀌지 않는다. */
231
+ "use-as-is",
232
+ /**
233
+ * **등급을 내려 다른 품목으로 판다** — 공정에 다시 들어가지 않고, 그 자리에서 품목 정의가 바뀐다.
234
+ *
235
+ * 다른 넷과 축이 다르다. 폐기도 특채도 아니고 **자재의 품목 자체가 달라진다**(GTIN 이 바뀐다).
236
+ * 식품·철강·섬유에서 일상이다.
237
+ */
238
+ "regrade",
239
+ /** 재고에서 뺀다. */
240
+ "scrap",
241
+ /** 공급자에게 돌려보낸다 — 재고에서 빠지되 폐기와 다른 사실이다. */
242
+ "return-to-supplier",
243
+ /** 아직 정하지 않았다 — 묶어 두고 쓰지 않는다. */
244
+ "hold"
245
+ ];
221
246
  function testPassedAt(r, at) {
222
247
  if (r.result !== "pass")
223
248
  return false;
@@ -556,6 +581,15 @@ var OP_EVENT = {
556
581
  * 이 채널이 없으면 시험 결과는 **상태에만 있는 축**이 된다 — 재기동에서 사라지고, 폴드가 되살릴 수
557
582
  * 없고, 미러가 이어받지 못한다(§상태 ⊆ 이벤트).
558
583
  */
584
+ /**
585
+ * **마감된 설비 상태 구간** — 이 설비가 이 구간 동안 이 상태였다.
586
+ *
587
+ * 시점의 전이(`equipment.status`)와 다른 사실이다. 전이는 설비가 내고, 이것은 **사람이 나중에
588
+ * 적는다.** 자재 대기처럼 신호를 내지 않는 정지는 이 길로만 들어온다.
589
+ *
590
+ * ISO 22400-2 의 가동률 계산이 이 구간들을 읽는다.
591
+ */
592
+ equipmentPeriod: "equipment.state.period",
559
593
  test: "test.result",
560
594
  /**
561
595
  * **부적합 처분** — 재고 판정을 받은 것을 어떻게 하기로 정했나(재작업 · 특채 · 폐기 · 반품).
@@ -779,7 +813,7 @@ function gs1Namespaces(prefix) {
779
813
  return [`urn:epc:idpat:sgtin:${prefix}.`, `urn:epc:class:lgtin:${prefix}.`, `urn:epc:id:sgtin:${prefix}.`];
780
814
  }
781
815
 
782
- // node_modules/@operato/ops-contract/dist/capacity.js
816
+ // ../ops-contract/dist/capacity.js
783
817
  function isoDurationHours(iso2) {
784
818
  if (!iso2)
785
819
  return 0;
@@ -887,7 +921,7 @@ function analyzeCapacity(input) {
887
921
  };
888
922
  }
889
923
 
890
- // node_modules/@operato/ops-contract/dist/wms-profile.js
924
+ // ../ops-contract/dist/wms-profile.js
891
925
  var BIZSTEP = {
892
926
  receiving: "urn:epcglobal:cbv:bizstep:receiving",
893
927
  storing: "urn:epcglobal:cbv:bizstep:storing",
@@ -917,7 +951,7 @@ var WMS_TYPES = [
917
951
  { key: "packer", role: "equipment", label: "twin.type.packer", standardClass: { epcis: "object", isa95: "Equipment" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] }
918
952
  ];
919
953
 
920
- // node_modules/@operato/ops-contract/dist/yms-profile.js
954
+ // ../ops-contract/dist/yms-profile.js
921
955
  var YARD_BIZSTEP = {
922
956
  arriving: "urn:epcglobal:cbv:bizstep:arriving",
923
957
  // 게이트-인
@@ -943,7 +977,7 @@ var YMS_TYPES = [
943
977
  { key: "hostler", role: "equipment", label: "twin.type.hostler", standardClass: { epcis: "object", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["mobile", "operable"] }
944
978
  ];
945
979
 
946
- // node_modules/@operato/ops-contract/dist/mes-profile.js
980
+ // ../ops-contract/dist/mes-profile.js
947
981
  var MES_BIZSTEP = {
948
982
  receiving: "urn:epcglobal:cbv:bizstep:receiving",
949
983
  // 원자재 수령
@@ -981,7 +1015,7 @@ var MES_TYPES = [
981
1015
  { key: "assembler", role: "equipment", label: "twin.type.assembler", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] }
982
1016
  ];
983
1017
 
984
- // node_modules/@operato/ops-contract/dist/ems-profile.js
1018
+ // ../ops-contract/dist/ems-profile.js
985
1019
  var EMS_LOCATION_TYPES = ["incoming", "feeder", "submeter-zone"];
986
1020
  var METER_DIRECTION = ["import", "export", "bidirectional"];
987
1021
  var EMS_PROPERTY = {
@@ -1302,7 +1336,7 @@ function isElectricalLocationType(type) {
1302
1336
  return EMS_LOCATION_TYPES.includes(type);
1303
1337
  }
1304
1338
 
1305
- // node_modules/@operato/ops-contract/dist/domain-catalog.js
1339
+ // ../ops-contract/dist/domain-catalog.js
1306
1340
  var locationKeys = (types) => types.filter((t) => t.role === "location").map((t) => t.key);
1307
1341
  var DOMAIN_CATALOG = {
1308
1342
  // label 은 언어 중립 i18n 키(twin.system.<code>) — 사람 언어는 표현계층이 렌더(L2).
@@ -1312,7 +1346,7 @@ var DOMAIN_CATALOG = {
1312
1346
  ems: { system: "ems", label: "twin.system.ems", types: EMS_TYPES, locationTypes: locationKeys(EMS_TYPES) }
1313
1347
  };
1314
1348
 
1315
- // node_modules/@operato/ops-contract/dist/domain-definition.js
1349
+ // ../ops-contract/dist/domain-definition.js
1316
1350
  var OP_PARAM = {
1317
1351
  /** 양품률(0..1, 무차원). 없으면 커널 기본값 — 기본값을 쓴 사실은 `specCoverage()` 가 밝힌다. */
1318
1352
  yield: "yield",
@@ -1321,7 +1355,7 @@ var OP_PARAM = {
1321
1355
  setupDuration: "setupDuration"
1322
1356
  };
1323
1357
 
1324
- // node_modules/@operato/ops-contract/dist/energy-ingest.js
1358
+ // ../ops-contract/dist/energy-ingest.js
1325
1359
  function resolveSubject(kind, localId, opts) {
1326
1360
  const declared = opts.identityOf?.(kind, localId);
1327
1361
  if (declared)
@@ -1332,7 +1366,7 @@ function periodFactId(tenantId, kind, subject, from, to) {
1332
1366
  return [tenantId, kind, subject, from, to].join("|");
1333
1367
  }
1334
1368
 
1335
- // node_modules/@operato/ops-contract/dist/epcis.js
1369
+ // ../ops-contract/dist/epcis.js
1336
1370
  var EPCIS_CONTEXT = "https://ref.gs1.org/standards/epcis/2.0.0/epcis-context.jsonld";
1337
1371
  var UTC_OFFSET = "+00:00";
1338
1372
  var DISP = {
@@ -1591,7 +1625,7 @@ function transformationEvent(p) {
1591
1625
  return e;
1592
1626
  }
1593
1627
 
1594
- // node_modules/@operato/ops-contract/dist/iso-duration.js
1628
+ // ../ops-contract/dist/iso-duration.js
1595
1629
  var RE = /^(-)?P(?:(\d+(?:\.\d+)?)W)?(?:(\d+(?:\.\d+)?)D)?(?:T(?:(\d+(?:\.\d+)?)H)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)S)?)?$/;
1596
1630
  function parseIsoDuration(text) {
1597
1631
  if (typeof text !== "string")
@@ -1617,7 +1651,7 @@ function parseIsoDuration(text) {
1617
1651
  return sign ? -ms2 : ms2;
1618
1652
  }
1619
1653
 
1620
- // node_modules/@operato/ops-contract/dist/master-data.js
1654
+ // ../ops-contract/dist/master-data.js
1621
1655
  function expiryFromAttributes(attrs) {
1622
1656
  return readEpochMs(attrs?.[ILMD_ATTR.expiry]);
1623
1657
  }
@@ -1634,9 +1668,9 @@ function readEpochMs(raw) {
1634
1668
  return Number.isNaN(at) ? void 0 : at;
1635
1669
  }
1636
1670
 
1637
- // node_modules/@operato/ops-contract/dist/operational-ingest.js
1671
+ // ../ops-contract/dist/operational-ingest.js
1638
1672
  var TASK_STATUS = ["created", "assigned", "in-progress", "completed"];
1639
- var EQUIPMENT_STATUS = ["idle", "busy", "down"];
1673
+ var EQUIPMENT_STATUS = ["idle", "busy", "down", "setup", "planned-stop"];
1640
1674
  var PERSON_STATUS = ["idle", "busy"];
1641
1675
  var ASSET_STATUS = ["idle", "in-use"];
1642
1676
  var SPECS = {
@@ -1694,6 +1728,8 @@ var SPECS = {
1694
1728
  effectiveStart: "string",
1695
1729
  effectiveEnd: "string",
1696
1730
  recordTime: "string",
1731
+ /* 왜 이 상태가 됐나 — 설비가 알려 주면 싣는다. 사람이 나중에 정하는 사유는 구간 사실에 적는다. */
1732
+ reasonCode: "string",
1697
1733
  /* 이동 구간 — 실 시스템도 줄 수 있는 사실이다(AGV·RTLS 가 출발·도착·소요를 낸다). 안쪽 필드까지
1698
1734
  재검사하지는 않는다: 그 모양은 `EquipmentMotion` 계약이고, 여기서 두 번 지키면 두 벌이 된다. */
1699
1735
  motion: "object"
@@ -1783,6 +1819,82 @@ var SPECS = {
1783
1819
  * `propertyMeasurements` 안쪽은 재검사하지 않는다 — 그 모양은 `PropertyMeasurement` 계약이고,
1784
1820
  * 여기서 두 번 지키면 두 벌이 된다(설비 `motion` 과 같은 규율).
1785
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
+ },
1865
+ /*
1866
+ * **부적합 처분** — 재고 판정을 받은 것을 어떻게 하기로 정했나.
1867
+ *
1868
+ * ── 이 통이 늦게 생긴 이유 (2026-08-30) ──────────────────────────────────
1869
+ * 사건 이름(`OP_EVENT.disposition`)과 모양(`DispositionFact`)을 먼저 만들고 **여기로 오는 길을
1870
+ * 내지 않았다.** 그래서 처분 사실을 보내면 어느 통도 아니어서 떨어진 목록에도 남지 않고 사라졌다.
1871
+ * MES 전문가가 소스를 읽다 찾았다.
1872
+ *
1873
+ * 이 파일의 위 주석이 말한 부류 그대로다 — `OP_EVENT.test` 와 `OP_EVENT.observation` 이 같은 일을
1874
+ * 겪었고 이번이 셋째다. **계약에 자리를 만드는 것과 그 자리로 가는 길을 내는 것은 다른 일이다.**
1875
+ *
1876
+ * ── 판정과 다른 사실이다 ─────────────────────────────────────────────────
1877
+ * 근거가 된 판정(`specId`)을 가리킬 수 있지만 **필수가 아니다** — 판정 없이 현장 재량으로 빼는 일이
1878
+ * 정상이다. 그래서 정체성은 처분 대상(`subjectId`)이다.
1879
+ */
1880
+ disposition: {
1881
+ eventType: OP_EVENT.disposition,
1882
+ identity: "subjectId",
1883
+ /* 무엇을 어떻게 하기로 했나 — 둘 중 하나가 없으면 그 결정은 아무 데도 붙지 못한다. */
1884
+ required: ["subjectId", "decision"],
1885
+ fields: {
1886
+ subjectId: "string",
1887
+ decision: "string",
1888
+ quantity: "number",
1889
+ uom: "string",
1890
+ specId: "string",
1891
+ decidedBy: "string",
1892
+ reason: "string",
1893
+ decidedAt: "string",
1894
+ recordTime: "string"
1895
+ },
1896
+ enums: { decision: DISPOSITION_DECISION }
1897
+ },
1786
1898
  test: {
1787
1899
  eventType: OP_EVENT.test,
1788
1900
  identity: "testableObjectId",
@@ -1842,7 +1954,7 @@ var SPECS = {
1842
1954
  }
1843
1955
  };
1844
1956
 
1845
- // node_modules/@operato/ops-contract/dist/operations-capability.js
1957
+ // ../ops-contract/dist/operations-capability.js
1846
1958
  function workingHoursBetween(calendar, fromMs, toMs, utcOffsetMinutes) {
1847
1959
  if (!(toMs > fromMs))
1848
1960
  return 0;
@@ -1912,6 +2024,39 @@ function operationsCapabilityOf(input) {
1912
2024
  };
1913
2025
  }
1914
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
+
1915
2060
  // src/divergence.ts
1916
2061
  function diffBy(predicted, actual, idOf, valOf) {
1917
2062
  const p = new Map(predicted.map((e) => [idOf(e), valOf(e)]));
@@ -3292,26 +3437,6 @@ function deriveAttentions(view, acked, nowIso, thresholds) {
3292
3437
  }
3293
3438
  return out;
3294
3439
  }
3295
- function computeOee(c, nowMs) {
3296
- const planned = c.metricsSinceMs == null ? 0 : Math.max(0, nowMs - c.metricsSinceMs - (c.holdMs ?? 0));
3297
- const uptime = Math.max(0, planned - c.setupMs - c.downMs);
3298
- const availability = planned > 0 ? uptime / planned : 1;
3299
- const performance = uptime > 0 ? Math.min(1, c.runMs / uptime) : c.runMs > 0 ? 1 : 0;
3300
- const totalQ = c.goodCount + c.scrapCount;
3301
- const quality = totalQ > 0 ? c.goodCount / totalQ : 1;
3302
- return {
3303
- availability,
3304
- performance,
3305
- quality,
3306
- overall: availability * performance * quality,
3307
- runMs: c.runMs,
3308
- setupMs: c.setupMs,
3309
- downMs: c.downMs,
3310
- idleMs: Math.max(0, uptime - c.runMs),
3311
- goodCount: c.goodCount,
3312
- scrapCount: c.scrapCount
3313
- };
3314
- }
3315
3440
  var ENERGY_STATE_FIELDS = [
3316
3441
  ...stateFieldsOf(["metered", "curtailable", "energyGenerating", "energyStoring", "switching"]),
3317
3442
  "measuredAt"
@@ -3936,7 +4061,7 @@ var FlowEngine = class {
3936
4061
  * 창은 지표 자신이 말해 준다 — 계획 시간은 가동·준비·고장·유휴의 합이다. 그래서 그 합만큼
3937
4062
  * 뒤로 물려 창을 다시 세운다. 지표가 없으면 창도 없다(그때는 지금부터 새로 잰다).
3938
4063
  */
3939
- ...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)) } : {},
3940
4065
  /* 계획 정지를 이어받는다 — 잃으면 씨앗이 **정비 중인 설비를 가용으로 놓고** 미래를 시뮬레이션한다
3941
4066
  (예측이 낙관 쪽으로 치우친다). 씨앗 왕복 대조가 이것을 잡았다. */
3942
4067
  ...m.held ? { held: true } : {},
@@ -9361,7 +9486,6 @@ function electricityCost(input) {
9361
9486
  YmsKernel,
9362
9487
  attributeEnergy,
9363
9488
  compareStates,
9364
- computeOee,
9365
9489
  constantDuration,
9366
9490
  counterfactualAt,
9367
9491
  demandWindowStart,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/twin-kernel",
3
- "version": "0.8.3",
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.3.0"
31
+ "@operato/ops-contract": "^0.5.0"
32
32
  }
33
33
  }