@operato/twin-kernel 0.11.1 → 0.11.2
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 +35 -3
- package/dist/flow-engine.js +84 -27
- package/dist/observed-reducer.d.ts +1 -1
- package/dist/observed-reducer.js +25 -7
- package/dist-cjs/index.cjs +123 -31
- package/package.json +1 -1
package/dist/flow-engine.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MaterialActual, 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
|
+
import { type RatedUsage, type UsedUsage } from '@operato/ops-contract';
|
|
2
3
|
import type { VocabularyElement } from '@operato/ops-contract';
|
|
3
4
|
import type { ReducerCheckpoint } from './observed-reducer.ts';
|
|
4
5
|
import type { EpcisEvent, BizTransactionElement } from '@operato/ops-contract';
|
|
@@ -51,6 +52,10 @@ export interface FlowAsset extends EffectivePeriod {
|
|
|
51
52
|
id: string;
|
|
52
53
|
/** 속한 등급들 — 표준 `PhysicalAssetClassID`(복수). */
|
|
53
54
|
assetClassIds?: string[];
|
|
55
|
+
/** 정격 사용량 — 선언(§RatedUsage). 이 자산이 얼마를 쓰면 교체되나. */
|
|
56
|
+
ratedUsage?: RatedUsage;
|
|
57
|
+
/** 지금까지 쓴 양 — 시뮬은 작업이 끝날 때 올리고, 미러는 원천이 말한 값을 든다. */
|
|
58
|
+
usedUsage?: UsedUsage;
|
|
54
59
|
location?: string;
|
|
55
60
|
status: string;
|
|
56
61
|
taskId: string | null;
|
|
@@ -109,6 +114,14 @@ export interface FlowEquipment extends EffectivePeriod {
|
|
|
109
114
|
location: string;
|
|
110
115
|
status: string;
|
|
111
116
|
taskId: string | null;
|
|
117
|
+
/**
|
|
118
|
+
* 속한 **등급들** — 표준 `EquipmentClassID`(복수). 사람·자산의 `personnelClassIds`·`assetClassIds` 와
|
|
119
|
+
* 같은 뜻이다.
|
|
120
|
+
*
|
|
121
|
+
* **판정에 이 필드를 직접 쓰지 않는다** — `equipmentClassMembership(eq)` 를 거친다. 소속은 이 선언과
|
|
122
|
+
* `kind` 를 합친 것이고, 합치는 규칙이 계약 한 자리에 있다(여섯 자리가 같은 답을 내야 한다).
|
|
123
|
+
*/
|
|
124
|
+
equipmentClassIds?: string[];
|
|
112
125
|
/** 붙박인 자리 — 소속. `location` 은 지금 위치. 고정 설비는 둘이 항상 같다(EquipmentState.homeLocation 참조). */
|
|
113
126
|
homeLocation?: string;
|
|
114
127
|
/** 자원 속성 — 표준 `EquipmentProperty`(§ResourceProperty). */
|
|
@@ -128,7 +141,7 @@ export interface FlowEquipment extends EffectivePeriod {
|
|
|
128
141
|
setupMs: number;
|
|
129
142
|
downMs: number;
|
|
130
143
|
goodCount: number;
|
|
131
|
-
|
|
144
|
+
nonconformingCount: number;
|
|
132
145
|
lastChangeoverKey?: string;
|
|
133
146
|
mtbfMs?: number;
|
|
134
147
|
mttrMs?: number;
|
|
@@ -204,7 +217,7 @@ export interface FlowTask {
|
|
|
204
217
|
* 이 작업의 **품질 판정 결과** — 판정이 있었을 때만 채운다 (2026-08-19).
|
|
205
218
|
*
|
|
206
219
|
* ── 왜 작업에 적나 ────────────────────────────────────────────────────────
|
|
207
|
-
* 양품/불량은 지금까지 두 곳에만 남았다: 설비 누적 카운터(`goodCount`·`
|
|
220
|
+
* 양품/불량은 지금까지 두 곳에만 남았다: 설비 누적 카운터(`goodCount`·`nonconformingCount` — 종류를 모른다)와
|
|
208
221
|
* EPCIS disposition(`sellable`/`non_sellable` — 종류를 모른다). 그래서 **「이 공정의 양품률이 얼마였나」**
|
|
209
222
|
* 를 되짚을 방법이 저널에 없었다 — 재기동하면 종류별 품질 이력이 통째로 사라진다.
|
|
210
223
|
*
|
|
@@ -359,7 +372,7 @@ export declare function deriveAttentions(view: {
|
|
|
359
372
|
status?: string;
|
|
360
373
|
location?: string;
|
|
361
374
|
goodCount?: number;
|
|
362
|
-
|
|
375
|
+
nonconformingCount?: number;
|
|
363
376
|
offShift?: boolean;
|
|
364
377
|
/**
|
|
365
378
|
* 이 설비 소식이 끊긴 시간, 그리고 그때까지 가장 오래 끊겼던 시간(밀리초).
|
|
@@ -1354,6 +1367,25 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
1354
1367
|
* 작업이 끝나면 자산을 놓아 준다 — **사람과 다른 점: 자산은 도착 자리에 남는다**(물건이므로).
|
|
1355
1368
|
* 싣고 있던 것은 놓는다(빈 팔레트로 돌아간다 — 회수·재사용의 출발점).
|
|
1356
1369
|
*/
|
|
1370
|
+
/**
|
|
1371
|
+
* 이 자산을 **한 번 썼다**고 센다 — 작업이 자산을 놓을 때.
|
|
1372
|
+
*
|
|
1373
|
+
* ── 셀 수 있는 것만 센다 ────────────────────────────────────────────────
|
|
1374
|
+
* 정격이 「번」(`USE_UOM`)이면 작업 하나가 곧 한 번이다. 그러면 올릴 수 있다.
|
|
1375
|
+
*
|
|
1376
|
+
* **정격이 「미터」면 올리지 않는다.** 그 작업이 몇 미터를 깎았는지는 공정이 말해 줘야 하는
|
|
1377
|
+
* 사실이고, 커널이 「한 번이니 1미터」로 놓으면 그것은 지어낸 값이다. 라우터 비트가 50미터
|
|
1378
|
+
* 정격인데 한 번에 0.2미터를 깎는다면, 1로 세는 순간 실제 수명의 250분의 1에서 교체하라고
|
|
1379
|
+
* 말하게 된다.
|
|
1380
|
+
*
|
|
1381
|
+
* 정격을 선언하지 않은 자산도 세지 않는다. 셀 이유가 없고(견줄 것이 없다), 세면 화면에 「쓴
|
|
1382
|
+
* 양은 있는데 정격은 없다」는 반쪽 사실이 생긴다.
|
|
1383
|
+
*
|
|
1384
|
+
* ── 미러에서는 부르지 않는다 ────────────────────────────────────────────
|
|
1385
|
+
* 이 함수는 시뮬레이션이 자기 우주에서 세는 것이다. 미러의 쓴 양은 **원천이 센 값**이고
|
|
1386
|
+
* (`AssetStatusDelta.usedUsage`), 우리가 거기에 더하면 같은 사용을 두 번 센다.
|
|
1387
|
+
*/
|
|
1388
|
+
private countOneUse;
|
|
1357
1389
|
private releaseAssets;
|
|
1358
1390
|
/**
|
|
1359
1391
|
* 필요 인원을 확보한다 — **등급으로 요구하고 등급으로 고른다**(특정인 지목이 아니다).
|
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, computeOee } from '@operato/ops-contract';
|
|
12
|
+
import { OP_EVENT, CMD, USE_UOM, locationStatusOf, readBoardEquipment, equipmentClassMembership, 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';
|
|
@@ -270,11 +270,11 @@ thresholds) {
|
|
|
270
270
|
}
|
|
271
271
|
}
|
|
272
272
|
for (const m of view.equipment) {
|
|
273
|
-
const total = (m.goodCount ?? 0) + (m.
|
|
273
|
+
const total = (m.goodCount ?? 0) + (m.nonconformingCount ?? 0);
|
|
274
274
|
const declaredMin = thresholds?.scrapMinSamplesOf?.(m.id);
|
|
275
275
|
const minSamples = declaredMin ?? ATTENTION_DEFAULTS.scrapMinSamples;
|
|
276
276
|
if (total >= minSamples) {
|
|
277
|
-
const rate = (m.
|
|
277
|
+
const rate = (m.nonconformingCount ?? 0) / total;
|
|
278
278
|
const declaredPct = thresholds?.scrapPctOf?.(m.id);
|
|
279
279
|
const thresholdPct = declaredPct ?? ATTENTION_DEFAULTS.scrapPct;
|
|
280
280
|
/* 「높음」은 기준의 두 배에서 — 기준이 현장의 것이면 그 두 배도 현장의 것이다(상수 두 벌을 두지 않는다). */
|
|
@@ -283,7 +283,7 @@ thresholds) {
|
|
|
283
283
|
id: `scrap:${m.id}`, kind: 'scrap-high', severity: rate * 100 >= thresholdPct * 2 ? 'high' : 'medium',
|
|
284
284
|
anchor: { moverId: m.id, locationId: m.location },
|
|
285
285
|
params: {
|
|
286
|
-
moverId: m.id, goodCount: m.goodCount ?? 0,
|
|
286
|
+
moverId: m.id, goodCount: m.goodCount ?? 0, nonconformingCount: m.nonconformingCount ?? 0, ratePct: Math.round(rate * 100),
|
|
287
287
|
thresholdPct, basis: declaredPct === undefined ? 'default' : 'declared'
|
|
288
288
|
},
|
|
289
289
|
recommendedActions: [
|
|
@@ -817,7 +817,7 @@ export class FlowEngine {
|
|
|
817
817
|
...(p.testSpecificationIds ? { testSpecificationIds: p.testSpecificationIds } : {}), ...(p.testResults ? { testResults: p.testResults } : {}), ...effectiveOnly(p) };
|
|
818
818
|
}
|
|
819
819
|
buildAsset(a) {
|
|
820
|
-
return { id: a.id, assetClassIds: a.assetClassIds, location: a.homeLocation, status: 'idle', taskId: null, ...(a.properties ? { properties: a.properties } : {}),
|
|
820
|
+
return { id: a.id, assetClassIds: a.assetClassIds, ...(a.ratedUsage ? { ratedUsage: a.ratedUsage } : {}), ...(a.usedUsage ? { usedUsage: a.usedUsage } : {}), location: a.homeLocation, status: 'idle', taskId: null, ...(a.properties ? { properties: a.properties } : {}),
|
|
821
821
|
...(a.testSpecificationIds ? { testSpecificationIds: a.testSpecificationIds } : {}), ...(a.testResults ? { testResults: a.testResults } : {}), ...effectiveOnly(a) };
|
|
822
822
|
}
|
|
823
823
|
/**
|
|
@@ -900,7 +900,7 @@ export class FlowEngine {
|
|
|
900
900
|
buildEquipment(m) {
|
|
901
901
|
/* **계측은 이 자원이 모델에 들어온 순간부터** 잰다(`metricsSinceMs`). 빠뜨리면 공식이 창을 모르고,
|
|
902
902
|
예전에는 그것을 epoch 로 메워 갓 뜬 설비가 56,000년 유휴로 보였다(§computeOee). */
|
|
903
|
-
const eq = { id: m.id, kind: m.kind, location: m.homeLocation, homeLocation: m.homeLocation, ...(m.properties ? { properties: m.properties } : {}), ...(m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}), ...(m.testResults ? { testResults: m.testResults } : {}), status: 'idle', taskId: null, runMs: 0, setupMs: 0, downMs: 0, goodCount: 0,
|
|
903
|
+
const eq = { id: m.id, kind: m.kind, ...(m.equipmentClassIds ? { equipmentClassIds: m.equipmentClassIds } : {}), location: m.homeLocation, homeLocation: m.homeLocation, ...(m.properties ? { properties: m.properties } : {}), ...(m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}), ...(m.testResults ? { testResults: m.testResults } : {}), status: 'idle', taskId: null, runMs: 0, setupMs: 0, downMs: 0, goodCount: 0, nonconformingCount: 0, metricsSinceMs: this.clockMs, window: m.window, ...(m.workCalendar ? { workCalendar: m.workCalendar } : {}), ...effectiveOnly(m) };
|
|
904
904
|
if (m.mtbfMs !== undefined) {
|
|
905
905
|
eq.mtbfMs = m.mtbfMs;
|
|
906
906
|
eq.mttrMs = m.mttrMs;
|
|
@@ -915,7 +915,7 @@ export class FlowEngine {
|
|
|
915
915
|
addEquipment(m) {
|
|
916
916
|
if (this.equipment.has(m.id))
|
|
917
917
|
return;
|
|
918
|
-
const eq = { id: m.id, kind: m.kind, location: m.homeLocation, homeLocation: m.homeLocation, ...(m.properties ? { properties: m.properties } : {}), ...(m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}), ...(m.testResults ? { testResults: m.testResults } : {}), status: 'idle', taskId: null, runMs: 0, setupMs: 0, downMs: 0, goodCount: 0,
|
|
918
|
+
const eq = { id: m.id, kind: m.kind, ...(m.equipmentClassIds ? { equipmentClassIds: m.equipmentClassIds } : {}), location: m.homeLocation, homeLocation: m.homeLocation, ...(m.properties ? { properties: m.properties } : {}), ...(m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}), ...(m.testResults ? { testResults: m.testResults } : {}), status: 'idle', taskId: null, runMs: 0, setupMs: 0, downMs: 0, goodCount: 0, nonconformingCount: 0, metricsSinceMs: this.clockMs };
|
|
919
919
|
if (m.mtbfMs !== undefined) {
|
|
920
920
|
eq.mtbfMs = m.mtbfMs;
|
|
921
921
|
eq.mttrMs = m.mttrMs;
|
|
@@ -1040,7 +1040,7 @@ export class FlowEngine {
|
|
|
1040
1040
|
for (const m of snap.equipment) {
|
|
1041
1041
|
/*
|
|
1042
1042
|
* 관측된 계측을 이어받는다 — **없어서 0 이었던 게 아니라 잘못 읽어서 0 이었다.**
|
|
1043
|
-
* `OeeMetrics` 의 이름은 `runMs`·`setupMs`·`downMs`·`goodCount`·`
|
|
1043
|
+
* `OeeMetrics` 의 이름은 `runMs`·`setupMs`·`downMs`·`goodCount`·`nonconformingCount` 인데 예전 코드가
|
|
1044
1044
|
* `oee.good`·`oee.scrap`(존재하지 않는 키)을 읽어 항상 0 으로 떨어졌고, 시간 누적은 아예 0 으로
|
|
1045
1045
|
* 시작했다. 그래서 씨앗이 "가동 이력이 전혀 없는 설비" 로 출발해 예측의 OEE 가 리셋됐다.
|
|
1046
1046
|
*
|
|
@@ -1056,8 +1056,13 @@ export class FlowEngine {
|
|
|
1056
1056
|
* 근무 시간을 모르고(항상 가용), 폐기를 모르고, 속도를 몰라 이동시간을 상수로 떨어뜨렸다.
|
|
1057
1057
|
* 관측이 말하는 것(위치·상태·계측)만 덮고 **선언은 지킨다**. */
|
|
1058
1058
|
const prev = this.equipment.get(m.id);
|
|
1059
|
+
/* 등급 소속은 **선언**이다 — 위 규율대로 마스터가 말한 것이 이긴다. 스냅샷의 값은 마스터가 아직
|
|
1060
|
+
실리지 않았을 때(관측만으로 되살리는 경로) 소속을 채운다. 둘 다 없으면 키를 만들지 않는다 —
|
|
1061
|
+
빈 배열을 심으면 "등급이 없는 설비" 와 "모르는 설비" 가 같아진다. */
|
|
1062
|
+
const classIds = prev?.equipmentClassIds ?? m.equipmentClassIds;
|
|
1059
1063
|
this.equipment.set(m.id, {
|
|
1060
1064
|
...(prev ?? {}),
|
|
1065
|
+
...(classIds ? { equipmentClassIds: classIds } : {}),
|
|
1061
1066
|
/*
|
|
1062
1067
|
* `taskId` — **관측이면 원본이 말한 값이다.**
|
|
1063
1068
|
*
|
|
@@ -1068,7 +1073,7 @@ export class FlowEngine {
|
|
|
1068
1073
|
*/
|
|
1069
1074
|
id: m.id, kind: m.kind, location: m.location ?? '', ...(m.homeLocation ? { homeLocation: m.homeLocation } : {}), status: m.status ?? 'idle', taskId: purpose === 'observe' ? (m.taskId ?? null) : null,
|
|
1070
1075
|
runMs: oee?.runMs ?? 0, setupMs: oee?.setupMs ?? 0, downMs: oee?.downMs ?? 0,
|
|
1071
|
-
goodCount: oee?.goodCount ?? 0,
|
|
1076
|
+
goodCount: oee?.goodCount ?? 0, nonconformingCount: oee?.nonconformingCount ?? 0,
|
|
1072
1077
|
/*
|
|
1073
1078
|
* **잰 값을 이어받으면 잰 구간도 이어받아야 한다.**
|
|
1074
1079
|
*
|
|
@@ -1140,7 +1145,12 @@ export class FlowEngine {
|
|
|
1140
1145
|
this.assets.set(a.id, {
|
|
1141
1146
|
...(prev ?? {}),
|
|
1142
1147
|
/* 설비·사람과 같은 규율 — 관측이면 원본이 말한 상태·묶임을 지킨다. */
|
|
1143
|
-
id: a.id, assetClassIds: a.assetClassIds ?? prev?.assetClassIds,
|
|
1148
|
+
id: a.id, assetClassIds: a.assetClassIds ?? prev?.assetClassIds,
|
|
1149
|
+
/* 정격은 선언이라 마스터가 이긴다. 쓴 양은 **관측이 이긴다** — 원천이 센 값이 우리 추정보다
|
|
1150
|
+
참이고, 되살릴 때 우리 것을 쓰면 그 사이 실제로 쓴 만큼이 사라진다. */
|
|
1151
|
+
...(prev?.ratedUsage ?? a.ratedUsage ? { ratedUsage: prev?.ratedUsage ?? a.ratedUsage } : {}),
|
|
1152
|
+
...(a.usedUsage ?? prev?.usedUsage ? { usedUsage: a.usedUsage ?? prev?.usedUsage } : {}),
|
|
1153
|
+
location: a.location,
|
|
1144
1154
|
status: purpose === 'observe' ? (a.status ?? 'idle') : 'idle',
|
|
1145
1155
|
taskId: purpose === 'observe' ? (a.taskId ?? null) : null, carrying: a.carrying,
|
|
1146
1156
|
...(a.properties ? { properties: a.properties } : {}),
|
|
@@ -1526,7 +1536,7 @@ export class FlowEngine {
|
|
|
1526
1536
|
m.setupMs = 0;
|
|
1527
1537
|
m.downMs = 0;
|
|
1528
1538
|
m.goodCount = 0;
|
|
1529
|
-
m.
|
|
1539
|
+
m.nonconformingCount = 0;
|
|
1530
1540
|
m.holdMs = 0;
|
|
1531
1541
|
m.metricsSinceMs = this.clockMs; // 계측 창을 지금부터 재시작
|
|
1532
1542
|
this.emitEquipment(m);
|
|
@@ -1634,7 +1644,7 @@ export class FlowEngine {
|
|
|
1634
1644
|
}),
|
|
1635
1645
|
items: [...this.items.values()].map(i => this.itemState(i)),
|
|
1636
1646
|
equipment: [...this.equipment.values()].map(m => {
|
|
1637
|
-
const s = { id: m.id, kind: m.kind, location: m.location, ...(m.homeLocation ? { homeLocation: m.homeLocation } : {}), status: m.status, taskId: m.taskId ?? undefined, oee: this.oeeOf(m), held: m.held, ...(m.properties ? { properties: m.properties } : {}), ...(m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}), ...(m.testResults ? { testResults: m.testResults } : {}), capability: this.capabilityOfResource(m, m
|
|
1647
|
+
const s = { id: m.id, kind: m.kind, ...(m.equipmentClassIds ? { equipmentClassIds: m.equipmentClassIds } : {}), location: m.location, ...(m.homeLocation ? { homeLocation: m.homeLocation } : {}), status: m.status, taskId: m.taskId ?? undefined, oee: this.oeeOf(m), held: m.held, ...(m.properties ? { properties: m.properties } : {}), ...(m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}), ...(m.testResults ? { testResults: m.testResults } : {}), capability: this.capabilityOfResource(m, equipmentClassMembership(m), this.classDefs.equipment), origin: 'master', ...(this.offShift(m) ? { offShift: true, ...this.offReason(m) } : {}), ...this.effectivePart(m), ...(this.shiftOf(m) ? { shift: this.shiftOf(m) } : {}), ...energyFieldsOf(m) };
|
|
1638
1648
|
const t = m.taskId ? this.tasks.get(m.taskId) : undefined;
|
|
1639
1649
|
/*
|
|
1640
1650
|
* 이동 구간은 **소요를 알 때만** 낸다. 모르면 아래 산수가 전부 0 이나 음수가 되고, 화면은 「제자리에서
|
|
@@ -1646,7 +1656,7 @@ export class FlowEngine {
|
|
|
1646
1656
|
return s;
|
|
1647
1657
|
}),
|
|
1648
1658
|
assets: [...this.assets.values()].map(a => {
|
|
1649
|
-
const st = { id: a.id, assetClassIds: a.assetClassIds, location: a.location, status: a.status, taskId: a.taskId ?? undefined };
|
|
1659
|
+
const st = { id: a.id, assetClassIds: a.assetClassIds, ...(a.ratedUsage ? { ratedUsage: a.ratedUsage } : {}), ...(a.usedUsage ? { usedUsage: a.usedUsage } : {}), location: a.location, status: a.status, taskId: a.taskId ?? undefined };
|
|
1650
1660
|
if (a.carrying)
|
|
1651
1661
|
st.carrying = a.carrying;
|
|
1652
1662
|
if (a.properties)
|
|
@@ -2308,7 +2318,7 @@ export class FlowEngine {
|
|
|
2308
2318
|
/* 판정 시각은 **구간의 시작**이다 — 능력은 그 구간을 두고 묻는 것이므로, 지금 고장 난 설비가 아니라
|
|
2309
2319
|
그 구간에 못 쓰는 설비를 세야 한다(같은 판정 함수에 다른 시각을 준다). */
|
|
2310
2320
|
const at = opts.window.fromTime;
|
|
2311
|
-
const usableEquipment = [...this.equipment.values()].filter(m => this.capabilityOfResource(m, m
|
|
2321
|
+
const usableEquipment = [...this.equipment.values()].filter(m => this.capabilityOfResource(m, equipmentClassMembership(m), this.classDefs.equipment, at).available);
|
|
2312
2322
|
const usablePersons = [...this.persons.values()].filter(p => this.capabilityOfResource(p, p.personnelClassIds, this.classDefs.personnel, at).available);
|
|
2313
2323
|
const usableAssets = [...this.assets.values()].filter(a => this.capabilityOfResource(a, a.assetClassIds, this.classDefs.asset, at).available);
|
|
2314
2324
|
const base = {
|
|
@@ -2725,9 +2735,9 @@ export class FlowEngine {
|
|
|
2725
2735
|
if (good)
|
|
2726
2736
|
m.goodCount++;
|
|
2727
2737
|
else
|
|
2728
|
-
m.
|
|
2738
|
+
m.nonconformingCount++;
|
|
2729
2739
|
// 품질 델타 방출 — live OEE 누적기가 good/scrap 을 정확 추적(equipment.status 는 quality 미포함). WMS/YMS 는 미호출→무영향.
|
|
2730
|
-
this.emitOp(OP_EVENT.quality, { moverId: m.id, good, goodCount: m.goodCount,
|
|
2740
|
+
this.emitOp(OP_EVENT.quality, { moverId: m.id, good, goodCount: m.goodCount, nonconformingCount: m.nonconformingCount });
|
|
2731
2741
|
}
|
|
2732
2742
|
/** 설비 OEE(스냅샷 파생) — Availability×Performance×Quality. planned = 설비 존재 sim 시간(clockMs). */
|
|
2733
2743
|
oeeOf(m) {
|
|
@@ -2846,7 +2856,7 @@ export class FlowEngine {
|
|
|
2846
2856
|
}
|
|
2847
2857
|
/** 사람 상태 전이 — 설비와 별개 채널(어휘가 다르다: 고장이 아니라 교대·투입). */
|
|
2848
2858
|
emitAsset(a) {
|
|
2849
|
-
this.emitOp(OP_EVENT.asset, { assetId: a.id, assetClassIds: a.assetClassIds, status: a.status, location: a.location, taskId: a.taskId ?? undefined, carrying: a.carrying, ...effectiveOnly(a) });
|
|
2859
|
+
this.emitOp(OP_EVENT.asset, { assetId: a.id, assetClassIds: a.assetClassIds, ...(a.ratedUsage ? { ratedUsage: a.ratedUsage } : {}), ...(a.usedUsage ? { usedUsage: a.usedUsage } : {}), status: a.status, location: a.location, taskId: a.taskId ?? undefined, carrying: a.carrying, ...effectiveOnly(a) });
|
|
2850
2860
|
}
|
|
2851
2861
|
emitPerson(p) {
|
|
2852
2862
|
this.emitOp(OP_EVENT.person, { personId: p.id, personnelClassIds: p.personnelClassIds, status: p.status, taskId: p.taskId ?? undefined, ...(p.location ? { location: p.location } : {}), ...effectiveOnly(p) });
|
|
@@ -2857,7 +2867,7 @@ export class FlowEngine {
|
|
|
2857
2867
|
/** 설비 상태 전이 — `taskId` 를 함께 싣는다(사람·자산 델타와 같은 규칙). 없으면 미러가 "이 설비가
|
|
2858
2868
|
* 무슨 일을 하는 중인가" 를 알 수 없어 작업↔자원 연결이 한쪽에서만 성립한다. */
|
|
2859
2869
|
emitEquipment(m, motion) {
|
|
2860
|
-
this.emitOp(OP_EVENT.equipment, { moverId: m.id, kind: m.kind, status: m.status, location: m.location, ...(m.homeLocation ? { homeLocation: m.homeLocation } : {}), taskId: m.taskId ?? undefined, motion, ...(m.held ? { held: true } : {}), ...effectiveOnly(m) });
|
|
2870
|
+
this.emitOp(OP_EVENT.equipment, { moverId: m.id, kind: m.kind, ...(m.equipmentClassIds ? { equipmentClassIds: m.equipmentClassIds } : {}), status: m.status, location: m.location, ...(m.homeLocation ? { homeLocation: m.homeLocation } : {}), taskId: m.taskId ?? undefined, motion, ...(m.held ? { held: true } : {}), ...effectiveOnly(m) });
|
|
2861
2871
|
}
|
|
2862
2872
|
/** 오더 델타 — **라인까지 싣는다.** 라인이 빠지면 미러가 남은 데맨드를 라인별로 재계획할 수 없다. */
|
|
2863
2873
|
emitOrder(o) {
|
|
@@ -2990,11 +3000,40 @@ export class FlowEngine {
|
|
|
2990
3000
|
* 작업이 끝나면 자산을 놓아 준다 — **사람과 다른 점: 자산은 도착 자리에 남는다**(물건이므로).
|
|
2991
3001
|
* 싣고 있던 것은 놓는다(빈 팔레트로 돌아간다 — 회수·재사용의 출발점).
|
|
2992
3002
|
*/
|
|
3003
|
+
/**
|
|
3004
|
+
* 이 자산을 **한 번 썼다**고 센다 — 작업이 자산을 놓을 때.
|
|
3005
|
+
*
|
|
3006
|
+
* ── 셀 수 있는 것만 센다 ────────────────────────────────────────────────
|
|
3007
|
+
* 정격이 「번」(`USE_UOM`)이면 작업 하나가 곧 한 번이다. 그러면 올릴 수 있다.
|
|
3008
|
+
*
|
|
3009
|
+
* **정격이 「미터」면 올리지 않는다.** 그 작업이 몇 미터를 깎았는지는 공정이 말해 줘야 하는
|
|
3010
|
+
* 사실이고, 커널이 「한 번이니 1미터」로 놓으면 그것은 지어낸 값이다. 라우터 비트가 50미터
|
|
3011
|
+
* 정격인데 한 번에 0.2미터를 깎는다면, 1로 세는 순간 실제 수명의 250분의 1에서 교체하라고
|
|
3012
|
+
* 말하게 된다.
|
|
3013
|
+
*
|
|
3014
|
+
* 정격을 선언하지 않은 자산도 세지 않는다. 셀 이유가 없고(견줄 것이 없다), 세면 화면에 「쓴
|
|
3015
|
+
* 양은 있는데 정격은 없다」는 반쪽 사실이 생긴다.
|
|
3016
|
+
*
|
|
3017
|
+
* ── 미러에서는 부르지 않는다 ────────────────────────────────────────────
|
|
3018
|
+
* 이 함수는 시뮬레이션이 자기 우주에서 세는 것이다. 미러의 쓴 양은 **원천이 센 값**이고
|
|
3019
|
+
* (`AssetStatusDelta.usedUsage`), 우리가 거기에 더하면 같은 사용을 두 번 센다.
|
|
3020
|
+
*/
|
|
3021
|
+
countOneUse(a) {
|
|
3022
|
+
const rated = a.ratedUsage;
|
|
3023
|
+
if (!rated || rated.uom !== USE_UOM)
|
|
3024
|
+
return;
|
|
3025
|
+
const prev = a.usedUsage;
|
|
3026
|
+
/* 단위가 어긋난 값에 더하지 않는다 — 더하면 두 단위가 한 수에 섞인다. */
|
|
3027
|
+
if (prev && prev.uom !== USE_UOM)
|
|
3028
|
+
return;
|
|
3029
|
+
a.usedUsage = { value: (prev?.value ?? 0) + 1, uom: USE_UOM, ...(prev?.since ? { since: prev.since } : {}) };
|
|
3030
|
+
}
|
|
2993
3031
|
releaseAssets(t) {
|
|
2994
3032
|
for (const id of t.assets ?? []) {
|
|
2995
3033
|
const a = this.assets.get(id);
|
|
2996
3034
|
if (!a)
|
|
2997
3035
|
continue;
|
|
3036
|
+
this.countOneUse(a);
|
|
2998
3037
|
a.status = 'idle';
|
|
2999
3038
|
a.taskId = null;
|
|
3000
3039
|
a.location = t.toNode || a.location;
|
|
@@ -3557,12 +3596,12 @@ export class FlowEngine {
|
|
|
3557
3596
|
const free = (kind, picked = []) => [...this.equipment.values()].filter(m =>
|
|
3558
3597
|
/* 사람과 **같은 판정**(§capabilityOfResource) — 예전에는 이 자리에 조건을 늘어놓아서
|
|
3559
3598
|
설비의 검사 결과는 배정에서 아예 읽히지 않았다(사람만 자격을 봤다). */
|
|
3560
|
-
this.capabilityOfResource(m, m
|
|
3599
|
+
this.capabilityOfResource(m, equipmentClassMembership(m), this.classDefs.equipment).available &&
|
|
3561
3600
|
!picked.includes(m.id) &&
|
|
3562
|
-
/*
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
(kind === undefined || classClosure(m
|
|
3601
|
+
/* 소속은 **선언한 등급들과 `kind` 를 합친 것**이다(§equipmentClassMembership). 상속도 탄다:
|
|
3602
|
+
'welder-6axis' 가 'welder' 를 상속하면 'welder' 요구를 만족한다. 예전에는 이 자리가
|
|
3603
|
+
`kind` 하나만 봤고, 그래서 `equipmentClasses` 정의표를 아무도 가리킬 수 없었다. */
|
|
3604
|
+
(kind === undefined || classClosure(equipmentClassMembership(m), this.classDefs.equipment, this.now()).has(kind)));
|
|
3566
3605
|
const need = this.operationSpecs.get(t.kind)?.equipmentSpecification;
|
|
3567
3606
|
if (!need?.length) {
|
|
3568
3607
|
const one = free(t.resourceType)[0];
|
|
@@ -3913,10 +3952,28 @@ export class FlowEngine {
|
|
|
3913
3952
|
this.consumeMaterials(t, stock);
|
|
3914
3953
|
this.emitTask(t);
|
|
3915
3954
|
// process: 제자리 변환(모션 없음). transport: 이동 모션 방출.
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3955
|
+
const motion = t.intent === 'process'
|
|
3956
|
+
? undefined
|
|
3957
|
+
: { fromNode: t.fromNode, toNode: t.toNode, startedAtSimMs: this.clockMs, durationMs: t.durationMs, progress: 0, elapsedMs: 0 };
|
|
3958
|
+
this.emitEquipment(eq, motion);
|
|
3959
|
+
/*
|
|
3960
|
+
* 함께 잡힌 설비도 이벤트를 낸다 — **해제 쪽과 대칭**(§advanceTasks: "함께 잡힌 설비도 같은
|
|
3961
|
+
* 규칙으로 놓아 준다").
|
|
3962
|
+
*
|
|
3963
|
+
* 이 자리가 대표 한 대만 냈다. 위에서 `rigs` 전부를 `busy` 로 바꾸는데 이벤트는 하나였으니
|
|
3964
|
+
* 미러는 나머지가 **잡힌 것을 못 보고 놓이는 것만 봤다** — 상태⊆이벤트 위반이고, 미러에서 그
|
|
3965
|
+
* 설비는 계속 유휴로 보인다(오류는 없다).
|
|
3966
|
+
*
|
|
3967
|
+
* 예전에는 등급 요구 여러 대를 잡는 일이 드물었다: 소속이 `kind` 하나였으니 "한 등급 2대" 를
|
|
3968
|
+
* 종류가 다른 설비로 채울 수 없었다. 소속을 복수로 열면서 이 경로가 흔해진다.
|
|
3969
|
+
*/
|
|
3970
|
+
for (const id of rigs) {
|
|
3971
|
+
if (id === eq.id)
|
|
3972
|
+
continue;
|
|
3973
|
+
const m = this.equipment.get(id);
|
|
3974
|
+
if (m)
|
|
3975
|
+
this.emitEquipment(m, motion);
|
|
3976
|
+
}
|
|
3920
3977
|
}
|
|
3921
3978
|
}
|
|
3922
3979
|
/** in-progress 진행/완료 — 상태 효과는 전부 도메인(onTaskComplete). base 는 자원 해제·델타만. */
|
package/dist/observed-reducer.js
CHANGED
|
@@ -43,7 +43,7 @@ function effectiveOf(r) {
|
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
45
|
function emptyOeeCounters() {
|
|
46
|
-
return { runMs: 0, setupMs: 0, downMs: 0, holdMs: 0, goodCount: 0,
|
|
46
|
+
return { runMs: 0, setupMs: 0, downMs: 0, holdMs: 0, goodCount: 0, nonconformingCount: 0 };
|
|
47
47
|
}
|
|
48
48
|
export class ObservedReducer {
|
|
49
49
|
/** 로케이션 마스터 — 출처를 함께 들고 있다(마스터가 말한 자리 vs 관측으로 알게 된 자리). */
|
|
@@ -128,7 +128,7 @@ export class ObservedReducer {
|
|
|
128
128
|
this.master.set(n.id, { id: n.id, type: n.type, capacity: n.capacity, parallelism: n.parallelism, parentId: n.parentId, origin: 'master' });
|
|
129
129
|
// 설비 기준선(마스터) — equipment.status 델타로 갱신됨.
|
|
130
130
|
for (const m of readBoardEquipment(model))
|
|
131
|
-
this.equipment.set(m.id, { id: m.id, kind: m.kind, status: 'idle', location: m.homeLocation, homeLocation: m.homeLocation, ...(m.properties ? { properties: m.properties } : {}), ...(m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}), ...(m.testResults ? { testResults: m.testResults } : {}), ...effectiveOf(m), origin: 'master' });
|
|
131
|
+
this.equipment.set(m.id, { id: m.id, kind: m.kind, ...(m.equipmentClassIds ? { equipmentClassIds: m.equipmentClassIds } : {}), status: 'idle', location: m.homeLocation, homeLocation: m.homeLocation, ...(m.properties ? { properties: m.properties } : {}), ...(m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}), ...(m.testResults ? { testResults: m.testResults } : {}), ...effectiveOf(m), origin: 'master' });
|
|
132
132
|
// 사람 기준선(마스터) — person.status 델타로 갱신됨.
|
|
133
133
|
for (const p of model.persons ?? [])
|
|
134
134
|
this.persons.set(p.id, { id: p.id, personnelClassIds: p.personnelClassIds, status: 'idle', ...(p.homeLocation ? { location: p.homeLocation } : {}), ...(p.properties ? { properties: p.properties } : {}), ...(p.testSpecificationIds ? { testSpecificationIds: p.testSpecificationIds } : {}), ...(p.testResults ? { testResults: p.testResults } : {}), ...effectiveOf(p) });
|
|
@@ -139,7 +139,7 @@ export class ObservedReducer {
|
|
|
139
139
|
for (const p of model.persons ?? [])
|
|
140
140
|
this.rememberShift(`person:${p.id}`, p);
|
|
141
141
|
for (const a of readBoardAssets(model))
|
|
142
|
-
this.assets.set(a.id, { id: a.id, assetClassIds: a.assetClassIds, location: a.homeLocation, status: 'idle', ...(a.properties ? { properties: a.properties } : {}), ...(a.testSpecificationIds ? { testSpecificationIds: a.testSpecificationIds } : {}), ...(a.testResults ? { testResults: a.testResults } : {}), ...effectiveOf(a) });
|
|
142
|
+
this.assets.set(a.id, { id: a.id, assetClassIds: a.assetClassIds, ...(a.ratedUsage ? { ratedUsage: a.ratedUsage } : {}), ...(a.usedUsage ? { usedUsage: a.usedUsage } : {}), location: a.homeLocation, status: 'idle', ...(a.properties ? { properties: a.properties } : {}), ...(a.testSpecificationIds ? { testSpecificationIds: a.testSpecificationIds } : {}), ...(a.testResults ? { testResults: a.testResults } : {}), ...effectiveOf(a) });
|
|
143
143
|
}
|
|
144
144
|
/**
|
|
145
145
|
* **구조를 전환한다** — 관측된 사실은 지키고 토폴로지만 새 선언으로 바꾼다.
|
|
@@ -196,7 +196,7 @@ export class ObservedReducer {
|
|
|
196
196
|
const assetsDropped = dropMissing(this.assets, assets);
|
|
197
197
|
for (const m of equipment) {
|
|
198
198
|
if (!this.equipment.has(m.id))
|
|
199
|
-
this.equipment.set(m.id, { id: m.id, kind: m.kind, status: 'idle', location: m.homeLocation, homeLocation: m.homeLocation, ...(m.properties ? { properties: m.properties } : {}), ...(m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}), ...(m.testResults ? { testResults: m.testResults } : {}), ...effectiveOf(m), origin: 'master' });
|
|
199
|
+
this.equipment.set(m.id, { id: m.id, kind: m.kind, ...(m.equipmentClassIds ? { equipmentClassIds: m.equipmentClassIds } : {}), status: 'idle', location: m.homeLocation, homeLocation: m.homeLocation, ...(m.properties ? { properties: m.properties } : {}), ...(m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}), ...(m.testResults ? { testResults: m.testResults } : {}), ...effectiveOf(m), origin: 'master' });
|
|
200
200
|
this.rememberShift(`eq:${m.id}`, m);
|
|
201
201
|
}
|
|
202
202
|
for (const p of persons) {
|
|
@@ -206,7 +206,7 @@ export class ObservedReducer {
|
|
|
206
206
|
}
|
|
207
207
|
for (const a of assets)
|
|
208
208
|
if (!this.assets.has(a.id))
|
|
209
|
-
this.assets.set(a.id, { id: a.id, assetClassIds: a.assetClassIds, location: a.homeLocation, status: 'idle', ...(a.properties ? { properties: a.properties } : {}), ...(a.testSpecificationIds ? { testSpecificationIds: a.testSpecificationIds } : {}), ...(a.testResults ? { testResults: a.testResults } : {}), ...effectiveOf(a) });
|
|
209
|
+
this.assets.set(a.id, { id: a.id, assetClassIds: a.assetClassIds, ...(a.ratedUsage ? { ratedUsage: a.ratedUsage } : {}), ...(a.usedUsage ? { usedUsage: a.usedUsage } : {}), location: a.homeLocation, status: 'idle', ...(a.properties ? { properties: a.properties } : {}), ...(a.testSpecificationIds ? { testSpecificationIds: a.testSpecificationIds } : {}), ...(a.testResults ? { testResults: a.testResults } : {}), ...effectiveOf(a) });
|
|
210
210
|
return { locationsAdded, locationsDropped, equipmentDropped, personsDropped, assetsDropped };
|
|
211
211
|
}
|
|
212
212
|
/** 마스터 동기 — 로케이션 추가/변경/제거. */
|
|
@@ -414,7 +414,8 @@ export class ObservedReducer {
|
|
|
414
414
|
판정은 여기서 하지 않는다 — `snapshot()` 이 관측 시각으로 `effectivityAt` 을 부른다. */
|
|
415
415
|
const eqPeriod = d.effectiveStart || d.effectiveEnd ? effectiveOf(d) : effectiveOf(known);
|
|
416
416
|
this.markHeard(d.moverId, e);
|
|
417
|
-
this.equipment.set(d.moverId, { id: d.moverId, kind: d.kind,
|
|
417
|
+
this.equipment.set(d.moverId, { id: d.moverId, kind: d.kind, /* 소속은 선언이라 관측이 지우지 않는다 — 이벤트가 실어 온 것이 있으면 그것, 없으면 이미 알던 것. */
|
|
418
|
+
...(d.equipmentClassIds ?? known?.equipmentClassIds ? { equipmentClassIds: d.equipmentClassIds ?? known?.equipmentClassIds } : {}), status: d.status, location: d.location, ...(homeLocation ? { homeLocation } : {}), taskId: d.taskId, motion: d.motion, origin: known?.origin ?? 'observed', ...eqPeriod, ...(d.held ? { held: true } : {}) });
|
|
418
419
|
break;
|
|
419
420
|
}
|
|
420
421
|
case OP_EVENT.person: {
|
|
@@ -471,11 +472,28 @@ export class ObservedReducer {
|
|
|
471
472
|
const d = e.data;
|
|
472
473
|
if (!d?.moverId)
|
|
473
474
|
break;
|
|
475
|
+
/*
|
|
476
|
+
* **두 수가 다 있어야 받는다.** 하나라도 수가 아니면 그 줄을 버린다.
|
|
477
|
+
*
|
|
478
|
+
* 예전에는 그대로 놓았다. 그러면 없는 값이 계수기에 `undefined` 로 앉고, 품질률의 분모
|
|
479
|
+
* `goodCount + nonconformingCount` 가 **NaN** 이 된다 — 그 뒤 모든 OEE 가 수가 아니게 되고
|
|
480
|
+
* 화면은 그것을 「빈 값」으로 그린다. 어느 줄이 그랬는지는 아무도 모른다.
|
|
481
|
+
*
|
|
482
|
+
* 실제로 그런 줄이 있다: `scrapCount` 시절의 저널 51행(실측). vocabulary-guard: allow
|
|
483
|
+
* 적힌 것이라 새 이름으로는 비어 있다. **읽어 주지 않는다** — 대개조 중에는 저장된 이름의
|
|
484
|
+
* 호환을 두지 않기로 했고, 두 키가 한 사실을 가리키는 것이 낡은 이름보다 나쁘다. 대신 그
|
|
485
|
+
* 줄을 조용히 0으로 세지 않고 **버린다**: 그 설비의 품질률은 「아직 모른다」로 남고, 다음
|
|
486
|
+
* 산출 보고가 오면 절대 누적이 그것을 다시 채운다.
|
|
487
|
+
*
|
|
488
|
+
* 이 검사는 커넥터가 필드를 빼먹은 경우도 같이 잡는다.
|
|
489
|
+
*/
|
|
490
|
+
if (!Number.isFinite(d.goodCount) || !Number.isFinite(d.nonconformingCount))
|
|
491
|
+
break;
|
|
474
492
|
if (this.stale(`quality:${d.moverId}`, e))
|
|
475
493
|
return;
|
|
476
494
|
const c = this.oeeCounters.get(d.moverId) ?? emptyOeeCounters();
|
|
477
495
|
c.goodCount = d.goodCount;
|
|
478
|
-
c.
|
|
496
|
+
c.nonconformingCount = d.nonconformingCount;
|
|
479
497
|
this.oeeCounters.set(d.moverId, c);
|
|
480
498
|
this.markHeard(d.moverId, e);
|
|
481
499
|
break;
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -197,6 +197,34 @@ function stateFieldsOf(caps) {
|
|
|
197
197
|
return [...out];
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
// ../ops-contract/dist/rated-usage.js
|
|
201
|
+
var USE_UOM = "C62";
|
|
202
|
+
var num = (n) => typeof n === "number" && Number.isFinite(n);
|
|
203
|
+
function ratedUsageOf(r) {
|
|
204
|
+
const rated = r?.ratedUsage;
|
|
205
|
+
const used = r?.usedUsage;
|
|
206
|
+
const missing = [];
|
|
207
|
+
const countable = rated?.uom === USE_UOM;
|
|
208
|
+
if (!rated)
|
|
209
|
+
missing.push("not-declared");
|
|
210
|
+
else if (!num(rated.limit) || rated.limit <= 0)
|
|
211
|
+
missing.push("invalid-limit");
|
|
212
|
+
if (!used || !num(used.value))
|
|
213
|
+
missing.push("usage-unknown");
|
|
214
|
+
if (rated && used && rated.uom !== used.uom)
|
|
215
|
+
missing.push("uom-mismatch");
|
|
216
|
+
const out = {
|
|
217
|
+
...rated ? { rated } : {},
|
|
218
|
+
...used ? { used } : {},
|
|
219
|
+
countable,
|
|
220
|
+
missing
|
|
221
|
+
};
|
|
222
|
+
if (missing.length)
|
|
223
|
+
return out;
|
|
224
|
+
const remaining = Math.max(0, rated.limit - used.value);
|
|
225
|
+
return { ...out, remaining, exhausted: used.value >= rated.limit };
|
|
226
|
+
}
|
|
227
|
+
|
|
200
228
|
// ../ops-contract/dist/contract.js
|
|
201
229
|
var ORDER_TERMINAL_STATUS = ["completed", "cancelled"];
|
|
202
230
|
function isOrderTerminal(o) {
|
|
@@ -366,6 +394,15 @@ function classClosure(directIds, defs, at) {
|
|
|
366
394
|
}
|
|
367
395
|
return out;
|
|
368
396
|
}
|
|
397
|
+
function equipmentClassMembership(e) {
|
|
398
|
+
const out = [];
|
|
399
|
+
for (const id of e?.equipmentClassIds ?? [])
|
|
400
|
+
if (id && !out.includes(id))
|
|
401
|
+
out.push(id);
|
|
402
|
+
if (e?.kind && !out.includes(e.kind))
|
|
403
|
+
out.push(e.kind);
|
|
404
|
+
return out;
|
|
405
|
+
}
|
|
369
406
|
var PRIORITY_UNSET = Number.POSITIVE_INFINITY;
|
|
370
407
|
function priorityRank(p) {
|
|
371
408
|
return typeof p === "number" && Number.isFinite(p) ? p : PRIORITY_UNSET;
|
|
@@ -543,6 +580,8 @@ function capabilityOf(r, ctx) {
|
|
|
543
580
|
return { available: false, reason: "held" };
|
|
544
581
|
if (r.status === "down")
|
|
545
582
|
return { available: false, reason: "down" };
|
|
583
|
+
if (ratedUsageOf(r).exhausted === true)
|
|
584
|
+
return { available: false, reason: "worn-out" };
|
|
546
585
|
if (at) {
|
|
547
586
|
const ms2 = parsedMs(at);
|
|
548
587
|
if (Number.isFinite(ms2)) {
|
|
@@ -1842,7 +1881,7 @@ var SPECS = {
|
|
|
1842
1881
|
identity: "moverId",
|
|
1843
1882
|
// vocabulary-guard: allow 저널 와이어 필드
|
|
1844
1883
|
/* 누적 카운터가 없으면 OEE 가 양품률을 못 센다 — 판정 하나만으로는 비율이 나오지 않는다. */
|
|
1845
|
-
required: ["moverId", "good", "goodCount", "
|
|
1884
|
+
required: ["moverId", "good", "goodCount", "nonconformingCount"],
|
|
1846
1885
|
// vocabulary-guard: allow
|
|
1847
1886
|
/*
|
|
1848
1887
|
* `taskId` 는 **귀속**이다 — 이 산출이 어느 작업의 것인가. 수량의 뜻은 바뀌지 않는다(설비 누적).
|
|
@@ -1853,7 +1892,7 @@ var SPECS = {
|
|
|
1853
1892
|
* 정체를 `taskId` 로 바꾸지 않는 이유는 OEE 가 설비 단위여서다. 정체를 옮기면 설비 누적을 셀 수
|
|
1854
1893
|
* 없어진다.
|
|
1855
1894
|
*/
|
|
1856
|
-
fields: { moverId: "string", good: "boolean", goodCount: "number",
|
|
1895
|
+
fields: { moverId: "string", good: "boolean", goodCount: "number", nonconformingCount: "number", taskId: "string", recordTime: "string" }
|
|
1857
1896
|
// vocabulary-guard: allow
|
|
1858
1897
|
},
|
|
1859
1898
|
/*
|
|
@@ -2090,7 +2129,7 @@ function computeOee(c, nowMs) {
|
|
|
2090
2129
|
missing.push("planned-busy-time");
|
|
2091
2130
|
const delayMs = Math.max(0, planned - c.runMs - c.setupMs - c.downMs);
|
|
2092
2131
|
const availability = planned > 0 ? Math.min(1, c.runMs / planned) : void 0;
|
|
2093
|
-
const produced = c.goodCount + c.
|
|
2132
|
+
const produced = c.goodCount + c.nonconformingCount;
|
|
2094
2133
|
if (produced <= 0)
|
|
2095
2134
|
missing.push("produced-quantity");
|
|
2096
2135
|
const quality = produced > 0 ? c.goodCount / produced : void 0;
|
|
@@ -2111,7 +2150,7 @@ function computeOee(c, nowMs) {
|
|
|
2111
2150
|
downMs: c.downMs,
|
|
2112
2151
|
delayMs,
|
|
2113
2152
|
goodCount: c.goodCount,
|
|
2114
|
-
|
|
2153
|
+
nonconformingCount: c.nonconformingCount
|
|
2115
2154
|
};
|
|
2116
2155
|
}
|
|
2117
2156
|
|
|
@@ -2352,7 +2391,7 @@ function effectiveOf(r) {
|
|
|
2352
2391
|
};
|
|
2353
2392
|
}
|
|
2354
2393
|
function emptyOeeCounters() {
|
|
2355
|
-
return { runMs: 0, setupMs: 0, downMs: 0, holdMs: 0, goodCount: 0,
|
|
2394
|
+
return { runMs: 0, setupMs: 0, downMs: 0, holdMs: 0, goodCount: 0, nonconformingCount: 0 };
|
|
2356
2395
|
}
|
|
2357
2396
|
var ObservedReducer = class {
|
|
2358
2397
|
/** 로케이션 마스터 — 출처를 함께 들고 있다(마스터가 말한 자리 vs 관측으로 알게 된 자리). */
|
|
@@ -2433,11 +2472,11 @@ var ObservedReducer = class {
|
|
|
2433
2472
|
this.classDefs = { personnel: model.personnelClasses, equipment: model.equipmentClasses, asset: model.assetClasses };
|
|
2434
2473
|
for (const sp of model.testSpecifications ?? []) this.testSpecs.set(sp.id, sp);
|
|
2435
2474
|
for (const n of readBoardLocations(model)) this.master.set(n.id, { id: n.id, type: n.type, capacity: n.capacity, parallelism: n.parallelism, parentId: n.parentId, origin: "master" });
|
|
2436
|
-
for (const m of readBoardEquipment(model)) this.equipment.set(m.id, { id: m.id, kind: m.kind, status: "idle", location: m.homeLocation, homeLocation: m.homeLocation, ...m.properties ? { properties: m.properties } : {}, ...m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}, ...m.testResults ? { testResults: m.testResults } : {}, ...effectiveOf(m), origin: "master" });
|
|
2475
|
+
for (const m of readBoardEquipment(model)) this.equipment.set(m.id, { id: m.id, kind: m.kind, ...m.equipmentClassIds ? { equipmentClassIds: m.equipmentClassIds } : {}, status: "idle", location: m.homeLocation, homeLocation: m.homeLocation, ...m.properties ? { properties: m.properties } : {}, ...m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}, ...m.testResults ? { testResults: m.testResults } : {}, ...effectiveOf(m), origin: "master" });
|
|
2437
2476
|
for (const p of model.persons ?? []) this.persons.set(p.id, { id: p.id, personnelClassIds: p.personnelClassIds, status: "idle", ...p.homeLocation ? { location: p.homeLocation } : {}, ...p.properties ? { properties: p.properties } : {}, ...p.testSpecificationIds ? { testSpecificationIds: p.testSpecificationIds } : {}, ...p.testResults ? { testResults: p.testResults } : {}, ...effectiveOf(p) });
|
|
2438
2477
|
for (const m of readBoardEquipment(model)) this.rememberShift(`eq:${m.id}`, m);
|
|
2439
2478
|
for (const p of model.persons ?? []) this.rememberShift(`person:${p.id}`, p);
|
|
2440
|
-
for (const a of readBoardAssets(model)) this.assets.set(a.id, { id: a.id, assetClassIds: a.assetClassIds, location: a.homeLocation, status: "idle", ...a.properties ? { properties: a.properties } : {}, ...a.testSpecificationIds ? { testSpecificationIds: a.testSpecificationIds } : {}, ...a.testResults ? { testResults: a.testResults } : {}, ...effectiveOf(a) });
|
|
2479
|
+
for (const a of readBoardAssets(model)) this.assets.set(a.id, { id: a.id, assetClassIds: a.assetClassIds, ...a.ratedUsage ? { ratedUsage: a.ratedUsage } : {}, ...a.usedUsage ? { usedUsage: a.usedUsage } : {}, location: a.homeLocation, status: "idle", ...a.properties ? { properties: a.properties } : {}, ...a.testSpecificationIds ? { testSpecificationIds: a.testSpecificationIds } : {}, ...a.testResults ? { testResults: a.testResults } : {}, ...effectiveOf(a) });
|
|
2441
2480
|
}
|
|
2442
2481
|
/**
|
|
2443
2482
|
* **구조를 전환한다** — 관측된 사실은 지키고 토폴로지만 새 선언으로 바꾼다.
|
|
@@ -2489,7 +2528,7 @@ var ObservedReducer = class {
|
|
|
2489
2528
|
const assetsDropped = dropMissing(this.assets, assets);
|
|
2490
2529
|
for (const m of equipment) {
|
|
2491
2530
|
if (!this.equipment.has(m.id))
|
|
2492
|
-
this.equipment.set(m.id, { id: m.id, kind: m.kind, status: "idle", location: m.homeLocation, homeLocation: m.homeLocation, ...m.properties ? { properties: m.properties } : {}, ...m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}, ...m.testResults ? { testResults: m.testResults } : {}, ...effectiveOf(m), origin: "master" });
|
|
2531
|
+
this.equipment.set(m.id, { id: m.id, kind: m.kind, ...m.equipmentClassIds ? { equipmentClassIds: m.equipmentClassIds } : {}, status: "idle", location: m.homeLocation, homeLocation: m.homeLocation, ...m.properties ? { properties: m.properties } : {}, ...m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}, ...m.testResults ? { testResults: m.testResults } : {}, ...effectiveOf(m), origin: "master" });
|
|
2493
2532
|
this.rememberShift(`eq:${m.id}`, m);
|
|
2494
2533
|
}
|
|
2495
2534
|
for (const p of persons) {
|
|
@@ -2499,7 +2538,7 @@ var ObservedReducer = class {
|
|
|
2499
2538
|
}
|
|
2500
2539
|
for (const a of assets)
|
|
2501
2540
|
if (!this.assets.has(a.id))
|
|
2502
|
-
this.assets.set(a.id, { id: a.id, assetClassIds: a.assetClassIds, location: a.homeLocation, status: "idle", ...a.properties ? { properties: a.properties } : {}, ...a.testSpecificationIds ? { testSpecificationIds: a.testSpecificationIds } : {}, ...a.testResults ? { testResults: a.testResults } : {}, ...effectiveOf(a) });
|
|
2541
|
+
this.assets.set(a.id, { id: a.id, assetClassIds: a.assetClassIds, ...a.ratedUsage ? { ratedUsage: a.ratedUsage } : {}, ...a.usedUsage ? { usedUsage: a.usedUsage } : {}, location: a.homeLocation, status: "idle", ...a.properties ? { properties: a.properties } : {}, ...a.testSpecificationIds ? { testSpecificationIds: a.testSpecificationIds } : {}, ...a.testResults ? { testResults: a.testResults } : {}, ...effectiveOf(a) });
|
|
2503
2542
|
return { locationsAdded, locationsDropped, equipmentDropped, personsDropped, assetsDropped };
|
|
2504
2543
|
}
|
|
2505
2544
|
/** 마스터 동기 — 로케이션 추가/변경/제거. */
|
|
@@ -2662,7 +2701,20 @@ var ObservedReducer = class {
|
|
|
2662
2701
|
const homeLocation = d.homeLocation ?? known?.homeLocation;
|
|
2663
2702
|
const eqPeriod = d.effectiveStart || d.effectiveEnd ? effectiveOf(d) : effectiveOf(known);
|
|
2664
2703
|
this.markHeard(d.moverId, e);
|
|
2665
|
-
this.equipment.set(d.moverId, {
|
|
2704
|
+
this.equipment.set(d.moverId, {
|
|
2705
|
+
id: d.moverId,
|
|
2706
|
+
kind: d.kind,
|
|
2707
|
+
/* 소속은 선언이라 관측이 지우지 않는다 — 이벤트가 실어 온 것이 있으면 그것, 없으면 이미 알던 것. */
|
|
2708
|
+
...d.equipmentClassIds ?? known?.equipmentClassIds ? { equipmentClassIds: d.equipmentClassIds ?? known?.equipmentClassIds } : {},
|
|
2709
|
+
status: d.status,
|
|
2710
|
+
location: d.location,
|
|
2711
|
+
...homeLocation ? { homeLocation } : {},
|
|
2712
|
+
taskId: d.taskId,
|
|
2713
|
+
motion: d.motion,
|
|
2714
|
+
origin: known?.origin ?? "observed",
|
|
2715
|
+
...eqPeriod,
|
|
2716
|
+
...d.held ? { held: true } : {}
|
|
2717
|
+
});
|
|
2666
2718
|
break;
|
|
2667
2719
|
}
|
|
2668
2720
|
case OP_EVENT.person: {
|
|
@@ -2704,10 +2756,11 @@ var ObservedReducer = class {
|
|
|
2704
2756
|
case OP_EVENT.quality: {
|
|
2705
2757
|
const d = e.data;
|
|
2706
2758
|
if (!d?.moverId) break;
|
|
2759
|
+
if (!Number.isFinite(d.goodCount) || !Number.isFinite(d.nonconformingCount)) break;
|
|
2707
2760
|
if (this.stale(`quality:${d.moverId}`, e)) return;
|
|
2708
2761
|
const c = this.oeeCounters.get(d.moverId) ?? emptyOeeCounters();
|
|
2709
2762
|
c.goodCount = d.goodCount;
|
|
2710
|
-
c.
|
|
2763
|
+
c.nonconformingCount = d.nonconformingCount;
|
|
2711
2764
|
this.oeeCounters.set(d.moverId, c);
|
|
2712
2765
|
this.markHeard(d.moverId, e);
|
|
2713
2766
|
break;
|
|
@@ -3513,11 +3566,11 @@ function deriveAttentions(view, acked, nowIso, thresholds) {
|
|
|
3513
3566
|
}
|
|
3514
3567
|
}
|
|
3515
3568
|
for (const m of view.equipment) {
|
|
3516
|
-
const total = (m.goodCount ?? 0) + (m.
|
|
3569
|
+
const total = (m.goodCount ?? 0) + (m.nonconformingCount ?? 0);
|
|
3517
3570
|
const declaredMin = thresholds?.scrapMinSamplesOf?.(m.id);
|
|
3518
3571
|
const minSamples = declaredMin ?? ATTENTION_DEFAULTS.scrapMinSamples;
|
|
3519
3572
|
if (total >= minSamples) {
|
|
3520
|
-
const rate = (m.
|
|
3573
|
+
const rate = (m.nonconformingCount ?? 0) / total;
|
|
3521
3574
|
const declaredPct = thresholds?.scrapPctOf?.(m.id);
|
|
3522
3575
|
const thresholdPct = declaredPct ?? ATTENTION_DEFAULTS.scrapPct;
|
|
3523
3576
|
if (rate * 100 >= thresholdPct) out.push({
|
|
@@ -3528,7 +3581,7 @@ function deriveAttentions(view, acked, nowIso, thresholds) {
|
|
|
3528
3581
|
params: {
|
|
3529
3582
|
moverId: m.id,
|
|
3530
3583
|
goodCount: m.goodCount ?? 0,
|
|
3531
|
-
|
|
3584
|
+
nonconformingCount: m.nonconformingCount ?? 0,
|
|
3532
3585
|
ratePct: Math.round(rate * 100),
|
|
3533
3586
|
thresholdPct,
|
|
3534
3587
|
basis: declaredPct === void 0 ? "default" : "declared"
|
|
@@ -3983,6 +4036,8 @@ var FlowEngine = class {
|
|
|
3983
4036
|
return {
|
|
3984
4037
|
id: a.id,
|
|
3985
4038
|
assetClassIds: a.assetClassIds,
|
|
4039
|
+
...a.ratedUsage ? { ratedUsage: a.ratedUsage } : {},
|
|
4040
|
+
...a.usedUsage ? { usedUsage: a.usedUsage } : {},
|
|
3986
4041
|
location: a.homeLocation,
|
|
3987
4042
|
status: "idle",
|
|
3988
4043
|
taskId: null,
|
|
@@ -4056,7 +4111,7 @@ var FlowEngine = class {
|
|
|
4056
4111
|
};
|
|
4057
4112
|
}
|
|
4058
4113
|
buildEquipment(m) {
|
|
4059
|
-
const eq = { id: m.id, kind: m.kind, location: m.homeLocation, homeLocation: m.homeLocation, ...m.properties ? { properties: m.properties } : {}, ...m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}, ...m.testResults ? { testResults: m.testResults } : {}, status: "idle", taskId: null, runMs: 0, setupMs: 0, downMs: 0, goodCount: 0,
|
|
4114
|
+
const eq = { id: m.id, kind: m.kind, ...m.equipmentClassIds ? { equipmentClassIds: m.equipmentClassIds } : {}, location: m.homeLocation, homeLocation: m.homeLocation, ...m.properties ? { properties: m.properties } : {}, ...m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}, ...m.testResults ? { testResults: m.testResults } : {}, status: "idle", taskId: null, runMs: 0, setupMs: 0, downMs: 0, goodCount: 0, nonconformingCount: 0, metricsSinceMs: this.clockMs, window: m.window, ...m.workCalendar ? { workCalendar: m.workCalendar } : {}, ...effectiveOnly(m) };
|
|
4060
4115
|
if (m.mtbfMs !== void 0) {
|
|
4061
4116
|
eq.mtbfMs = m.mtbfMs;
|
|
4062
4117
|
eq.mttrMs = m.mttrMs;
|
|
@@ -4070,7 +4125,7 @@ var FlowEngine = class {
|
|
|
4070
4125
|
*/
|
|
4071
4126
|
addEquipment(m) {
|
|
4072
4127
|
if (this.equipment.has(m.id)) return;
|
|
4073
|
-
const eq = { id: m.id, kind: m.kind, location: m.homeLocation, homeLocation: m.homeLocation, ...m.properties ? { properties: m.properties } : {}, ...m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}, ...m.testResults ? { testResults: m.testResults } : {}, status: "idle", taskId: null, runMs: 0, setupMs: 0, downMs: 0, goodCount: 0,
|
|
4128
|
+
const eq = { id: m.id, kind: m.kind, ...m.equipmentClassIds ? { equipmentClassIds: m.equipmentClassIds } : {}, location: m.homeLocation, homeLocation: m.homeLocation, ...m.properties ? { properties: m.properties } : {}, ...m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}, ...m.testResults ? { testResults: m.testResults } : {}, status: "idle", taskId: null, runMs: 0, setupMs: 0, downMs: 0, goodCount: 0, nonconformingCount: 0, metricsSinceMs: this.clockMs };
|
|
4074
4129
|
if (m.mtbfMs !== void 0) {
|
|
4075
4130
|
eq.mtbfMs = m.mtbfMs;
|
|
4076
4131
|
eq.mttrMs = m.mttrMs;
|
|
@@ -4166,8 +4221,10 @@ var FlowEngine = class {
|
|
|
4166
4221
|
for (const m of snap.equipment) {
|
|
4167
4222
|
const oee = m.oee;
|
|
4168
4223
|
const prev = this.equipment.get(m.id);
|
|
4224
|
+
const classIds = prev?.equipmentClassIds ?? m.equipmentClassIds;
|
|
4169
4225
|
this.equipment.set(m.id, {
|
|
4170
4226
|
...prev ?? {},
|
|
4227
|
+
...classIds ? { equipmentClassIds: classIds } : {},
|
|
4171
4228
|
/*
|
|
4172
4229
|
* `taskId` — **관측이면 원본이 말한 값이다.**
|
|
4173
4230
|
*
|
|
@@ -4186,7 +4243,7 @@ var FlowEngine = class {
|
|
|
4186
4243
|
setupMs: oee?.setupMs ?? 0,
|
|
4187
4244
|
downMs: oee?.downMs ?? 0,
|
|
4188
4245
|
goodCount: oee?.goodCount ?? 0,
|
|
4189
|
-
|
|
4246
|
+
nonconformingCount: oee?.nonconformingCount ?? 0,
|
|
4190
4247
|
/*
|
|
4191
4248
|
* **잰 값을 이어받으면 잰 구간도 이어받아야 한다.**
|
|
4192
4249
|
*
|
|
@@ -4255,6 +4312,10 @@ var FlowEngine = class {
|
|
|
4255
4312
|
/* 설비·사람과 같은 규율 — 관측이면 원본이 말한 상태·묶임을 지킨다. */
|
|
4256
4313
|
id: a.id,
|
|
4257
4314
|
assetClassIds: a.assetClassIds ?? prev?.assetClassIds,
|
|
4315
|
+
/* 정격은 선언이라 마스터가 이긴다. 쓴 양은 **관측이 이긴다** — 원천이 센 값이 우리 추정보다
|
|
4316
|
+
참이고, 되살릴 때 우리 것을 쓰면 그 사이 실제로 쓴 만큼이 사라진다. */
|
|
4317
|
+
...prev?.ratedUsage ?? a.ratedUsage ? { ratedUsage: prev?.ratedUsage ?? a.ratedUsage } : {},
|
|
4318
|
+
...a.usedUsage ?? prev?.usedUsage ? { usedUsage: a.usedUsage ?? prev?.usedUsage } : {},
|
|
4258
4319
|
location: a.location,
|
|
4259
4320
|
status: purpose === "observe" ? a.status ?? "idle" : "idle",
|
|
4260
4321
|
taskId: purpose === "observe" ? a.taskId ?? null : null,
|
|
@@ -4542,7 +4603,7 @@ var FlowEngine = class {
|
|
|
4542
4603
|
m.setupMs = 0;
|
|
4543
4604
|
m.downMs = 0;
|
|
4544
4605
|
m.goodCount = 0;
|
|
4545
|
-
m.
|
|
4606
|
+
m.nonconformingCount = 0;
|
|
4546
4607
|
m.holdMs = 0;
|
|
4547
4608
|
m.metricsSinceMs = this.clockMs;
|
|
4548
4609
|
this.emitEquipment(m);
|
|
@@ -4638,14 +4699,14 @@ var FlowEngine = class {
|
|
|
4638
4699
|
}),
|
|
4639
4700
|
items: [...this.items.values()].map((i) => this.itemState(i)),
|
|
4640
4701
|
equipment: [...this.equipment.values()].map((m) => {
|
|
4641
|
-
const s = { id: m.id, kind: m.kind, location: m.location, ...m.homeLocation ? { homeLocation: m.homeLocation } : {}, status: m.status, taskId: m.taskId ?? void 0, oee: this.oeeOf(m), held: m.held, ...m.properties ? { properties: m.properties } : {}, ...m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}, ...m.testResults ? { testResults: m.testResults } : {}, capability: this.capabilityOfResource(m, m
|
|
4702
|
+
const s = { id: m.id, kind: m.kind, ...m.equipmentClassIds ? { equipmentClassIds: m.equipmentClassIds } : {}, location: m.location, ...m.homeLocation ? { homeLocation: m.homeLocation } : {}, status: m.status, taskId: m.taskId ?? void 0, oee: this.oeeOf(m), held: m.held, ...m.properties ? { properties: m.properties } : {}, ...m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}, ...m.testResults ? { testResults: m.testResults } : {}, capability: this.capabilityOfResource(m, equipmentClassMembership(m), this.classDefs.equipment), origin: "master", ...this.offShift(m) ? { offShift: true, ...this.offReason(m) } : {}, ...this.effectivePart(m), ...this.shiftOf(m) ? { shift: this.shiftOf(m) } : {}, ...energyFieldsOf(m) };
|
|
4642
4703
|
const t = m.taskId ? this.tasks.get(m.taskId) : void 0;
|
|
4643
4704
|
const motionProgress = t && t.status === "in-progress" && t.intent !== "process" ? this.progressOf(t) : void 0;
|
|
4644
4705
|
if (t && motionProgress !== void 0) s.motion = { fromNode: t.fromNode, toNode: t.toNode, startedAtSimMs: this.clockMs - (t.durationMs - t.remainingMs), durationMs: t.durationMs, progress: motionProgress, elapsedMs: t.durationMs - t.remainingMs };
|
|
4645
4706
|
return s;
|
|
4646
4707
|
}),
|
|
4647
4708
|
assets: [...this.assets.values()].map((a) => {
|
|
4648
|
-
const st = { id: a.id, assetClassIds: a.assetClassIds, location: a.location, status: a.status, taskId: a.taskId ?? void 0 };
|
|
4709
|
+
const st = { id: a.id, assetClassIds: a.assetClassIds, ...a.ratedUsage ? { ratedUsage: a.ratedUsage } : {}, ...a.usedUsage ? { usedUsage: a.usedUsage } : {}, location: a.location, status: a.status, taskId: a.taskId ?? void 0 };
|
|
4649
4710
|
if (a.carrying) st.carrying = a.carrying;
|
|
4650
4711
|
if (a.properties) st.properties = a.properties;
|
|
4651
4712
|
if (a.testSpecificationIds) st.testSpecificationIds = a.testSpecificationIds;
|
|
@@ -5220,7 +5281,7 @@ var FlowEngine = class {
|
|
|
5220
5281
|
const calendar = calendars.size === 1 ? [...calendars.values()][0] : void 0;
|
|
5221
5282
|
const at = opts.window.fromTime;
|
|
5222
5283
|
const usableEquipment = [...this.equipment.values()].filter(
|
|
5223
|
-
(m) => this.capabilityOfResource(m, m
|
|
5284
|
+
(m) => this.capabilityOfResource(m, equipmentClassMembership(m), this.classDefs.equipment, at).available
|
|
5224
5285
|
);
|
|
5225
5286
|
const usablePersons = [...this.persons.values()].filter(
|
|
5226
5287
|
(p) => this.capabilityOfResource(p, p.personnelClassIds, this.classDefs.personnel, at).available
|
|
@@ -5581,8 +5642,8 @@ var FlowEngine = class {
|
|
|
5581
5642
|
const m = moverId ? this.equipment.get(moverId) : void 0;
|
|
5582
5643
|
if (!m) return;
|
|
5583
5644
|
if (good) m.goodCount++;
|
|
5584
|
-
else m.
|
|
5585
|
-
this.emitOp(OP_EVENT.quality, { moverId: m.id, good, goodCount: m.goodCount,
|
|
5645
|
+
else m.nonconformingCount++;
|
|
5646
|
+
this.emitOp(OP_EVENT.quality, { moverId: m.id, good, goodCount: m.goodCount, nonconformingCount: m.nonconformingCount });
|
|
5586
5647
|
}
|
|
5587
5648
|
/** 설비 OEE(스냅샷 파생) — Availability×Performance×Quality. planned = 설비 존재 sim 시간(clockMs). */
|
|
5588
5649
|
oeeOf(m) {
|
|
@@ -5698,7 +5759,7 @@ var FlowEngine = class {
|
|
|
5698
5759
|
}
|
|
5699
5760
|
/** 사람 상태 전이 — 설비와 별개 채널(어휘가 다르다: 고장이 아니라 교대·투입). */
|
|
5700
5761
|
emitAsset(a) {
|
|
5701
|
-
this.emitOp(OP_EVENT.asset, { assetId: a.id, assetClassIds: a.assetClassIds, status: a.status, location: a.location, taskId: a.taskId ?? void 0, carrying: a.carrying, ...effectiveOnly(a) });
|
|
5762
|
+
this.emitOp(OP_EVENT.asset, { assetId: a.id, assetClassIds: a.assetClassIds, ...a.ratedUsage ? { ratedUsage: a.ratedUsage } : {}, ...a.usedUsage ? { usedUsage: a.usedUsage } : {}, status: a.status, location: a.location, taskId: a.taskId ?? void 0, carrying: a.carrying, ...effectiveOnly(a) });
|
|
5702
5763
|
}
|
|
5703
5764
|
emitPerson(p) {
|
|
5704
5765
|
this.emitOp(OP_EVENT.person, { personId: p.id, personnelClassIds: p.personnelClassIds, status: p.status, taskId: p.taskId ?? void 0, ...p.location ? { location: p.location } : {}, ...effectiveOnly(p) });
|
|
@@ -5706,7 +5767,7 @@ var FlowEngine = class {
|
|
|
5706
5767
|
/** 설비 상태 전이 — `taskId` 를 함께 싣는다(사람·자산 델타와 같은 규칙). 없으면 미러가 "이 설비가
|
|
5707
5768
|
* 무슨 일을 하는 중인가" 를 알 수 없어 작업↔자원 연결이 한쪽에서만 성립한다. */
|
|
5708
5769
|
emitEquipment(m, motion) {
|
|
5709
|
-
this.emitOp(OP_EVENT.equipment, { moverId: m.id, kind: m.kind, status: m.status, location: m.location, ...m.homeLocation ? { homeLocation: m.homeLocation } : {}, taskId: m.taskId ?? void 0, motion, ...m.held ? { held: true } : {}, ...effectiveOnly(m) });
|
|
5770
|
+
this.emitOp(OP_EVENT.equipment, { moverId: m.id, kind: m.kind, ...m.equipmentClassIds ? { equipmentClassIds: m.equipmentClassIds } : {}, status: m.status, location: m.location, ...m.homeLocation ? { homeLocation: m.homeLocation } : {}, taskId: m.taskId ?? void 0, motion, ...m.held ? { held: true } : {}, ...effectiveOnly(m) });
|
|
5710
5771
|
}
|
|
5711
5772
|
/** 오더 델타 — **라인까지 싣는다.** 라인이 빠지면 미러가 남은 데맨드를 라인별로 재계획할 수 없다. */
|
|
5712
5773
|
emitOrder(o) {
|
|
@@ -5829,10 +5890,36 @@ var FlowEngine = class {
|
|
|
5829
5890
|
* 작업이 끝나면 자산을 놓아 준다 — **사람과 다른 점: 자산은 도착 자리에 남는다**(물건이므로).
|
|
5830
5891
|
* 싣고 있던 것은 놓는다(빈 팔레트로 돌아간다 — 회수·재사용의 출발점).
|
|
5831
5892
|
*/
|
|
5893
|
+
/**
|
|
5894
|
+
* 이 자산을 **한 번 썼다**고 센다 — 작업이 자산을 놓을 때.
|
|
5895
|
+
*
|
|
5896
|
+
* ── 셀 수 있는 것만 센다 ────────────────────────────────────────────────
|
|
5897
|
+
* 정격이 「번」(`USE_UOM`)이면 작업 하나가 곧 한 번이다. 그러면 올릴 수 있다.
|
|
5898
|
+
*
|
|
5899
|
+
* **정격이 「미터」면 올리지 않는다.** 그 작업이 몇 미터를 깎았는지는 공정이 말해 줘야 하는
|
|
5900
|
+
* 사실이고, 커널이 「한 번이니 1미터」로 놓으면 그것은 지어낸 값이다. 라우터 비트가 50미터
|
|
5901
|
+
* 정격인데 한 번에 0.2미터를 깎는다면, 1로 세는 순간 실제 수명의 250분의 1에서 교체하라고
|
|
5902
|
+
* 말하게 된다.
|
|
5903
|
+
*
|
|
5904
|
+
* 정격을 선언하지 않은 자산도 세지 않는다. 셀 이유가 없고(견줄 것이 없다), 세면 화면에 「쓴
|
|
5905
|
+
* 양은 있는데 정격은 없다」는 반쪽 사실이 생긴다.
|
|
5906
|
+
*
|
|
5907
|
+
* ── 미러에서는 부르지 않는다 ────────────────────────────────────────────
|
|
5908
|
+
* 이 함수는 시뮬레이션이 자기 우주에서 세는 것이다. 미러의 쓴 양은 **원천이 센 값**이고
|
|
5909
|
+
* (`AssetStatusDelta.usedUsage`), 우리가 거기에 더하면 같은 사용을 두 번 센다.
|
|
5910
|
+
*/
|
|
5911
|
+
countOneUse(a) {
|
|
5912
|
+
const rated = a.ratedUsage;
|
|
5913
|
+
if (!rated || rated.uom !== USE_UOM) return;
|
|
5914
|
+
const prev = a.usedUsage;
|
|
5915
|
+
if (prev && prev.uom !== USE_UOM) return;
|
|
5916
|
+
a.usedUsage = { value: (prev?.value ?? 0) + 1, uom: USE_UOM, ...prev?.since ? { since: prev.since } : {} };
|
|
5917
|
+
}
|
|
5832
5918
|
releaseAssets(t) {
|
|
5833
5919
|
for (const id of t.assets ?? []) {
|
|
5834
5920
|
const a = this.assets.get(id);
|
|
5835
5921
|
if (!a) continue;
|
|
5922
|
+
this.countOneUse(a);
|
|
5836
5923
|
a.status = "idle";
|
|
5837
5924
|
a.taskId = null;
|
|
5838
5925
|
a.location = t.toNode || a.location;
|
|
@@ -6290,10 +6377,10 @@ var FlowEngine = class {
|
|
|
6290
6377
|
(m) => (
|
|
6291
6378
|
/* 사람과 **같은 판정**(§capabilityOfResource) — 예전에는 이 자리에 조건을 늘어놓아서
|
|
6292
6379
|
설비의 검사 결과는 배정에서 아예 읽히지 않았다(사람만 자격을 봤다). */
|
|
6293
|
-
this.capabilityOfResource(m, m
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
(kind === void 0 || classClosure(m
|
|
6380
|
+
this.capabilityOfResource(m, equipmentClassMembership(m), this.classDefs.equipment).available && !picked2.includes(m.id) && /* 소속은 **선언한 등급들과 `kind` 를 합친 것**이다(§equipmentClassMembership). 상속도 탄다:
|
|
6381
|
+
'welder-6axis' 가 'welder' 를 상속하면 'welder' 요구를 만족한다. 예전에는 이 자리가
|
|
6382
|
+
`kind` 하나만 봤고, 그래서 `equipmentClasses` 정의표를 아무도 가리킬 수 없었다. */
|
|
6383
|
+
(kind === void 0 || classClosure(equipmentClassMembership(m), this.classDefs.equipment, this.now()).has(kind))
|
|
6297
6384
|
)
|
|
6298
6385
|
);
|
|
6299
6386
|
const need = this.operationSpecs.get(t.kind)?.equipmentSpecification;
|
|
@@ -6592,8 +6679,13 @@ var FlowEngine = class {
|
|
|
6592
6679
|
this.assignAssets(t, gear);
|
|
6593
6680
|
this.consumeMaterials(t, stock);
|
|
6594
6681
|
this.emitTask(t);
|
|
6595
|
-
|
|
6596
|
-
|
|
6682
|
+
const motion = t.intent === "process" ? void 0 : { fromNode: t.fromNode, toNode: t.toNode, startedAtSimMs: this.clockMs, durationMs: t.durationMs, progress: 0, elapsedMs: 0 };
|
|
6683
|
+
this.emitEquipment(eq, motion);
|
|
6684
|
+
for (const id of rigs) {
|
|
6685
|
+
if (id === eq.id) continue;
|
|
6686
|
+
const m = this.equipment.get(id);
|
|
6687
|
+
if (m) this.emitEquipment(m, motion);
|
|
6688
|
+
}
|
|
6597
6689
|
}
|
|
6598
6690
|
}
|
|
6599
6691
|
/** in-progress 진행/완료 — 상태 효과는 전부 도메인(onTaskComplete). base 는 자원 해제·델타만. */
|
package/package.json
CHANGED