@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.
- package/dist/flow-engine.d.ts +1 -17
- package/dist/flow-engine.js +3 -29
- package/dist-cjs/index.cjs +163 -39
- package/package.json +2 -2
package/dist/flow-engine.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TestResult, ISOTime, MaterialQuantity, WorkCalendarEntry, EffectivePeriod, Effectivity, OffCalendarReason, ResourceProperty, ResourceClassDef, MaterialDefinition, Attention, TwinModelDef, CanonicalEnvelope, Command, CommandAck, EventHandler, EquipmentMotion,
|
|
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
|
*
|
package/dist/flow-engine.js
CHANGED
|
@@ -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
|
-
/* `
|
|
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.
|
|
1082
|
+
? { metricsSinceMs: this.clockMs - ((oee.runMs ?? 0) + (oee.setupMs ?? 0) + (oee.downMs ?? 0) + (oee.delayMs ?? 0)) }
|
|
1109
1083
|
: {}),
|
|
1110
1084
|
/* 계획 정지를 이어받는다 — 잃으면 씨앗이 **정비 중인 설비를 가용으로 놓고** 미래를 시뮬레이션한다
|
|
1111
1085
|
(예측이 낙관 쪽으로 치우친다). 씨앗 왕복 대조가 이것을 잡았다. */
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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.
|
|
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.
|
|
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.
|
|
31
|
+
"@operato/ops-contract": "^0.5.0"
|
|
32
32
|
}
|
|
33
33
|
}
|