@operato/twin-kernel 0.6.6 → 0.6.8
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/contract.d.ts +97 -0
- package/dist/contract.js +54 -0
- package/dist/flow-engine.d.ts +22 -1
- package/dist/flow-engine.js +53 -37
- package/dist/observed-reducer.d.ts +14 -0
- package/dist/observed-reducer.js +31 -10
- package/dist-cjs/index.cjs +112 -39
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -216,6 +216,45 @@ export declare function testPassedAt(r: TestResult, at?: ISOTime): boolean;
|
|
|
216
216
|
* 없는 것으로 막지 않는다). 결과가 있으면 그것이 유효한 합격이어야 한다.
|
|
217
217
|
*/
|
|
218
218
|
export declare function meetsTests(required: readonly string[], results: readonly TestResult[] | undefined, at?: ISOTime): boolean;
|
|
219
|
+
/**
|
|
220
|
+
* 자원이 **지금 쓰일 수 있나, 아니면 왜 못 쓰이나** — ISA-95 `PersonnelCapability`/`EquipmentCapability`.
|
|
221
|
+
*
|
|
222
|
+
* ── 왜 계약이 이것을 소유해야 하나 ────────────────────────────────────────────
|
|
223
|
+
* 커널은 배정할 때 이미 이 판정을 한다(교대·고장·보류·유효기간·시험 만료). 그런데 그 규칙이 **엔진 안의
|
|
224
|
+
* 필터 조건으로만** 있어서, 화면은 같은 판정을 자기 코드로 다시 만들었다(`reasonOf`). 규칙이 두 벌이면
|
|
225
|
+
* 반드시 갈라진다 — 배정은 막는데 화면은 "가용" 이라 말하는 순간이 온다. 그 어긋남은 조용하다.
|
|
226
|
+
*
|
|
227
|
+
* 그래서 **이유까지 계약이 낸다.** 화면·예측·AI 가 같은 낱말로 말하고, 새 이유가 생기면(시험 만료가
|
|
228
|
+
* 그랬다) 한 곳만 늘어난다.
|
|
229
|
+
*
|
|
230
|
+
* ── 이유의 순서가 뜻이다 ──────────────────────────────────────────────────────
|
|
231
|
+
* 여러 이유가 겹칠 수 있다(폐기한 설비가 고장 상태로 남아 있는 것). **먼저 오는 것을 답한다** —
|
|
232
|
+
* "이미 모델 밖" 이 "고장" 보다 앞선다(폐기한 설비의 고장은 고칠 일이 아니다).
|
|
233
|
+
*/
|
|
234
|
+
export type CapabilityReason =
|
|
235
|
+
/** 유효기간 전 — 아직 없는 자원(도입 예정). 기다릴 일이다. */
|
|
236
|
+
'not-yet'
|
|
237
|
+
/** 유효기간 후 — 이미 없는 자원(폐기·퇴사). 지울 일이다. */
|
|
238
|
+
| 'retired'
|
|
239
|
+
/** 사람이 막았다(`held`) — 지시로 보류. */
|
|
240
|
+
| 'held'
|
|
241
|
+
/** 고장 — 설비만. 고칠 일이다. */
|
|
242
|
+
| 'down'
|
|
243
|
+
/** 근무·가동 시간 밖(교대 사이) — 기다리면 돌아온다. */
|
|
244
|
+
| 'off-shift'
|
|
245
|
+
/** 근무일이 아니다(휴일) — 하루 통째로 쉰다. `off-shift` 와 기다릴 시간이 다르다. */
|
|
246
|
+
| 'resting'
|
|
247
|
+
/** 요구된 시험의 결과가 만료·불합격 — 자격이 성립하지 않는다(§TestResult). */
|
|
248
|
+
| 'test-expired'
|
|
249
|
+
/** 지금 다른 일을 하고 있다 — 능력은 있고 여유가 없다. */
|
|
250
|
+
| 'working'
|
|
251
|
+
/** 쓸 수 있다. */
|
|
252
|
+
| 'available';
|
|
253
|
+
/** 가용 여부와 그 이유 — `available` 이면 `reason: 'available'`. */
|
|
254
|
+
export interface Capability {
|
|
255
|
+
available: boolean;
|
|
256
|
+
reason: CapabilityReason;
|
|
257
|
+
}
|
|
219
258
|
export interface TestSpecification {
|
|
220
259
|
/** 표준 `ID` — 자원의 `testSpecificationIds` 가 이 값을 가리킨다. */
|
|
221
260
|
id: string;
|
|
@@ -578,6 +617,36 @@ export declare function offCalendarAt(r: {
|
|
|
578
617
|
};
|
|
579
618
|
workCalendar?: WorkCalendarEntry[];
|
|
580
619
|
}, ms: number, utcOffsetMinutes?: number): boolean;
|
|
620
|
+
/**
|
|
621
|
+
* 이 자원에게 **필수인 시험 목록** — 등급 상속을 타고 닫아 모은다.
|
|
622
|
+
*
|
|
623
|
+
* 요구는 등급이 말하고(`ResourceClassDef.testSpecificationIds`) 기록은 개체가 든다(`testResults`).
|
|
624
|
+
* 그 사이를 잇는 이 수집이 계약에 있는 이유: **두 구동이 같은 답을 내야 한다.** 시뮬과 미러가 각자
|
|
625
|
+
* 모으면 "이 사람에게 무엇이 필수인가" 가 갈리고, 같은 자원을 한쪽은 막고 다른 쪽은 통과시킨다.
|
|
626
|
+
*/
|
|
627
|
+
export declare function requiredTestsFor(directIds: readonly string[] | undefined, defs: readonly ResourceClassDef[] | undefined, at?: ISOTime): string[];
|
|
628
|
+
/**
|
|
629
|
+
* 자원의 **가용 능력을 판정한다** — 하나의 규칙, 하나의 자리.
|
|
630
|
+
*
|
|
631
|
+
* 부르는 쪽이 시각과 시간대를 준다(커널은 `now()`·`utcOffsetMinutes`, 호스트는 관측 시각). 주지 않으면
|
|
632
|
+
* 시각에 달린 판정(유효기간·교대·시험 만료)은 **하지 않는다** — 모르면 판단하지 않는다는 규율이다.
|
|
633
|
+
*
|
|
634
|
+
* `requiredTests` 는 부르는 쪽이 등급에서 모아 넘긴다(등급 정의를 아는 것은 부르는 쪽이다).
|
|
635
|
+
*/
|
|
636
|
+
export declare function capabilityOf(r: {
|
|
637
|
+
status?: string;
|
|
638
|
+
held?: boolean;
|
|
639
|
+
window?: {
|
|
640
|
+
startHour: number;
|
|
641
|
+
endHour: number;
|
|
642
|
+
};
|
|
643
|
+
workCalendar?: WorkCalendarEntry[];
|
|
644
|
+
testResults?: TestResult[];
|
|
645
|
+
} & EffectivePeriod, ctx?: {
|
|
646
|
+
at?: ISOTime;
|
|
647
|
+
utcOffsetMinutes?: number;
|
|
648
|
+
requiredTests?: readonly string[];
|
|
649
|
+
}): Capability;
|
|
581
650
|
export interface LocationState {
|
|
582
651
|
id: string;
|
|
583
652
|
type: string;
|
|
@@ -796,6 +865,18 @@ export interface EquipmentState extends EffectivePeriod {
|
|
|
796
865
|
* 없으면 판정하지 않는다(없는 것으로 막으면 자격자가 전부 사라진다).
|
|
797
866
|
*/
|
|
798
867
|
testResults?: TestResult[];
|
|
868
|
+
/**
|
|
869
|
+
* **왜 지금 쓰일 수 없나** — 판정과 사유를 계약이 낸다(`capabilityOf` · `CapabilityReason`).
|
|
870
|
+
*
|
|
871
|
+
* 상태에 실어 보내는 이유: 이 판정은 **시각과 등급 정의를 함께 알아야** 나온다(유효기간·교대·자격
|
|
872
|
+
* 만료). 소비처는 그 둘을 갖고 있지 않다 — 화면은 시뮬 시각을 모르고, 호스트는 등급 상속을 타고
|
|
873
|
+
* 필수 시험을 모으는 규칙을 모른다. 그래서 예전에는 소비처가 플래그를 보고 짐작했고, 자격이 만료된
|
|
874
|
+
* 사람이 `대기` 로 보였다. 놀고 있는 것은 맞지만 **쓸 수 있는 것은 아니다** — "대기 7명" 이 조용히
|
|
875
|
+
* 틀린 숫자가 됐다.
|
|
876
|
+
*
|
|
877
|
+
* 배정이 쓰는 판정과 **같은 함수의 결과**다(규칙 한 벌). 사유의 순서도 계약이 정한다.
|
|
878
|
+
*/
|
|
879
|
+
capability?: Capability;
|
|
799
880
|
}
|
|
800
881
|
/**
|
|
801
882
|
* 사람 — **ISA-95 `Person`.** 설비와 다른 자원 종류다.
|
|
@@ -861,6 +942,18 @@ export interface PersonState extends EffectivePeriod {
|
|
|
861
942
|
* 없으면 판정하지 않는다(없는 것으로 막으면 자격자가 전부 사라진다).
|
|
862
943
|
*/
|
|
863
944
|
testResults?: TestResult[];
|
|
945
|
+
/**
|
|
946
|
+
* **왜 지금 쓰일 수 없나** — 판정과 사유를 계약이 낸다(`capabilityOf` · `CapabilityReason`).
|
|
947
|
+
*
|
|
948
|
+
* 상태에 실어 보내는 이유: 이 판정은 **시각과 등급 정의를 함께 알아야** 나온다(유효기간·교대·자격
|
|
949
|
+
* 만료). 소비처는 그 둘을 갖고 있지 않다 — 화면은 시뮬 시각을 모르고, 호스트는 등급 상속을 타고
|
|
950
|
+
* 필수 시험을 모으는 규칙을 모른다. 그래서 예전에는 소비처가 플래그를 보고 짐작했고, 자격이 만료된
|
|
951
|
+
* 사람이 `대기` 로 보였다. 놀고 있는 것은 맞지만 **쓸 수 있는 것은 아니다** — "대기 7명" 이 조용히
|
|
952
|
+
* 틀린 숫자가 됐다.
|
|
953
|
+
*
|
|
954
|
+
* 배정이 쓰는 판정과 **같은 함수의 결과**다(규칙 한 벌). 사유의 순서도 계약이 정한다.
|
|
955
|
+
*/
|
|
956
|
+
capability?: Capability;
|
|
864
957
|
}
|
|
865
958
|
/**
|
|
866
959
|
* 물리 자산 — **ISA-95 `PhysicalAsset`, GS1 `GRAI`(반복사용 자산).**
|
|
@@ -901,6 +994,10 @@ export interface AssetState extends EffectivePeriod {
|
|
|
901
994
|
* 빠뜨리면 회수 대상 수가 실제보다 많게 잡히고, 빈 팔레트 부족이 보이지 않는다.
|
|
902
995
|
*/
|
|
903
996
|
effectivity?: Effectivity;
|
|
997
|
+
/** 그 시험들의 결과 — 사람과 같은 뜻(§TestResult). 검사에서 떨어진 팔레트는 배정에서 빠진다. */
|
|
998
|
+
testResults?: TestResult[];
|
|
999
|
+
/** 왜 지금 쓰일 수 없나 — 사람·설비와 **같은 판정**(§PersonState.capability). */
|
|
1000
|
+
capability?: Capability;
|
|
904
1001
|
}
|
|
905
1002
|
/**
|
|
906
1003
|
* 작업 하나의 관측 상태 — **ISA-95 `SegmentResponse`**(Part 4, 실적).
|
package/dist/contract.js
CHANGED
|
@@ -517,6 +517,60 @@ export function offCalendarAt(r, ms, utcOffsetMinutes) {
|
|
|
517
517
|
const h = Math.floor(minute / 60);
|
|
518
518
|
return !(w.startHour <= w.endHour ? h >= w.startHour && h < w.endHour : h >= w.startHour || h < w.endHour);
|
|
519
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* 이 자원에게 **필수인 시험 목록** — 등급 상속을 타고 닫아 모은다.
|
|
522
|
+
*
|
|
523
|
+
* 요구는 등급이 말하고(`ResourceClassDef.testSpecificationIds`) 기록은 개체가 든다(`testResults`).
|
|
524
|
+
* 그 사이를 잇는 이 수집이 계약에 있는 이유: **두 구동이 같은 답을 내야 한다.** 시뮬과 미러가 각자
|
|
525
|
+
* 모으면 "이 사람에게 무엇이 필수인가" 가 갈리고, 같은 자원을 한쪽은 막고 다른 쪽은 통과시킨다.
|
|
526
|
+
*/
|
|
527
|
+
export function requiredTestsFor(directIds, defs, at) {
|
|
528
|
+
if (!defs?.length)
|
|
529
|
+
return [];
|
|
530
|
+
const closure = classClosure(directIds, defs, at);
|
|
531
|
+
const required = [];
|
|
532
|
+
for (const d of defs)
|
|
533
|
+
if (closure.has(d.id))
|
|
534
|
+
required.push(...(d.testSpecificationIds ?? []));
|
|
535
|
+
return required;
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* 자원의 **가용 능력을 판정한다** — 하나의 규칙, 하나의 자리.
|
|
539
|
+
*
|
|
540
|
+
* 부르는 쪽이 시각과 시간대를 준다(커널은 `now()`·`utcOffsetMinutes`, 호스트는 관측 시각). 주지 않으면
|
|
541
|
+
* 시각에 달린 판정(유효기간·교대·시험 만료)은 **하지 않는다** — 모르면 판단하지 않는다는 규율이다.
|
|
542
|
+
*
|
|
543
|
+
* `requiredTests` 는 부르는 쪽이 등급에서 모아 넘긴다(등급 정의를 아는 것은 부르는 쪽이다).
|
|
544
|
+
*/
|
|
545
|
+
export function capabilityOf(r, ctx) {
|
|
546
|
+
const at = ctx?.at;
|
|
547
|
+
/* 순서가 뜻이다 — "이미 모델 밖" 이 "고장" 보다 앞선다(폐기한 설비의 고장은 고칠 일이 아니다). */
|
|
548
|
+
const eff = effectivityAt(r, at);
|
|
549
|
+
if (eff === 'not-yet')
|
|
550
|
+
return { available: false, reason: 'not-yet' };
|
|
551
|
+
if (eff === 'expired')
|
|
552
|
+
return { available: false, reason: 'retired' };
|
|
553
|
+
if (r.held)
|
|
554
|
+
return { available: false, reason: 'held' };
|
|
555
|
+
if (r.status === 'down')
|
|
556
|
+
return { available: false, reason: 'down' };
|
|
557
|
+
if (at) {
|
|
558
|
+
const ms = Date.parse(at);
|
|
559
|
+
if (Number.isFinite(ms)) {
|
|
560
|
+
const why = offCalendarReasonAt(r, ms, ctx?.utcOffsetMinutes);
|
|
561
|
+
if (why === 'non-working')
|
|
562
|
+
return { available: false, reason: 'resting' };
|
|
563
|
+
if (why === 'off-hours')
|
|
564
|
+
return { available: false, reason: 'off-shift' };
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
/* 자격은 **결과가 선언됐을 때만** 제약이다(§meetsTests) — 없는 것으로 막으면 라인이 굶는다. */
|
|
568
|
+
if (ctx?.requiredTests?.length && !meetsTests(ctx.requiredTests, r.testResults, at))
|
|
569
|
+
return { available: false, reason: 'test-expired' };
|
|
570
|
+
if (r.status && r.status !== 'idle' && r.status !== 'available')
|
|
571
|
+
return { available: false, reason: 'working' };
|
|
572
|
+
return { available: true, reason: 'available' };
|
|
573
|
+
}
|
|
520
574
|
// ── 운영 델타(비-EPCIS) — State 채널의 나머지 절반 ──────────────────────────
|
|
521
575
|
// EPCIS 이벤트는 재고/위치만 재구성 가능. tasks·equipment·orders 의 운영 상태는
|
|
522
576
|
// 이 델타로 미러한다. envelope.eventType = 'task.status' | 'equipment.status' | 'order.status'.
|
package/dist/flow-engine.d.ts
CHANGED
|
@@ -56,6 +56,13 @@ export interface FlowAsset extends EffectivePeriod {
|
|
|
56
56
|
properties?: ResourceProperty[];
|
|
57
57
|
/** 적격을 검증한 시험 명세들 — 표준 `TestSpecificationID`. */
|
|
58
58
|
testSpecificationIds?: string[];
|
|
59
|
+
/**
|
|
60
|
+
* 그 시험들의 **결과** — 자격이 성립하는지는 이것이 말한다(§TestResult).
|
|
61
|
+
*
|
|
62
|
+
* 사람에게만 두었다가 **설비·자산의 결과는 커널이 받아서 버렸다** — 선언은 들어오는데 자리가
|
|
63
|
+
* 없으니 배정 판정에서 아예 읽히지 않았다(검사에서 떨어진 도장 부스가 계속 배정됐다).
|
|
64
|
+
*/
|
|
65
|
+
testResults?: TestResult[];
|
|
59
66
|
}
|
|
60
67
|
/** 사람 — 설비와 별개 자원(고장·OEE 가 아니라 등급·교대로 산다). */
|
|
61
68
|
export interface FlowPerson extends EffectivePeriod {
|
|
@@ -96,6 +103,8 @@ export interface FlowEquipment extends EffectivePeriod {
|
|
|
96
103
|
properties?: ResourceProperty[];
|
|
97
104
|
/** 적격을 검증한 시험 명세들 — 표준 `TestSpecificationID`. */
|
|
98
105
|
testSpecificationIds?: string[];
|
|
106
|
+
/** 그 시험들의 **결과** — 사람과 같은 뜻(§FlowAsset.testResults). 없으면 판정하지 않는다. */
|
|
107
|
+
testResults?: TestResult[];
|
|
99
108
|
/** 교대(가동시간) — 지정 시 이 시간대에만 배정된다. 미지정=24시간 가용. */
|
|
100
109
|
window?: {
|
|
101
110
|
startHour: number;
|
|
@@ -809,7 +818,19 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
809
818
|
* 판정 규칙은 계약이 소유한다(`meetsTests`) — 여기서 다시 적으면 규칙이 두 곳이 되고, 한쪽이 만료를
|
|
810
819
|
* 조용히 통과시킨다.
|
|
811
820
|
*/
|
|
812
|
-
|
|
821
|
+
/**
|
|
822
|
+
* 이 사람이 속한 등급들이 **요구하는 시험 목록** — 판정은 계약이 한다(`capabilityOf`).
|
|
823
|
+
*
|
|
824
|
+
* 상속을 타고 닫은 등급 전부의 요구를 모은다: 상위 등급이 요구하는 시험도 자격의 조건이다.
|
|
825
|
+
*/
|
|
826
|
+
/**
|
|
827
|
+
* 자원 하나의 **가용 능력** — 계약의 판정에 이 커널의 시각·시간대·필수 시험을 채워 넘긴다.
|
|
828
|
+
*
|
|
829
|
+
* 배정도 스냅샷도 이 자리를 지난다. 예전에는 배정이 조건을 늘어놓고 화면이 플래그를 보고 짐작해서,
|
|
830
|
+
* **자격이 만료된 사람이 화면에서는 `대기` 로 보였다**(놀고 있는 것은 맞지만 쓸 수 있는 것은 아니다).
|
|
831
|
+
* 판정에 필요한 둘(시각·등급 상속)을 아는 것은 커널뿐이므로, 답도 커널이 낸다.
|
|
832
|
+
*/
|
|
833
|
+
private capabilityOfResource;
|
|
813
834
|
private claimPersonnel;
|
|
814
835
|
/**
|
|
815
836
|
* 필요 설비를 고른다 — **인원·자산과 같은 규칙**(등급으로 요구, 부분 확보 없이 전량 아니면 대기).
|
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,
|
|
12
|
+
import { OP_EVENT, CMD, locationStatusOf, readBoardEquipment, readBoardLocations, readBoardAssets, classClosure, capabilityOf, requiredTestsFor, priorityRank, dueStatusOf, effectivityAt, offCalendarAt, offCalendarReasonAt, minuteOfDayAt, activeShiftAt, subLotIdOf, itemKeyOf } from "./contract.js";
|
|
13
13
|
import { ObservedReducer } from "./observed-reducer.js";
|
|
14
14
|
import { transformationEvent, aggregationEvent, objectEvent, parseEpc, DISP, ILMD_ATTR, CBV_BIZSTEP } from "./epcis.js";
|
|
15
15
|
import { parseIsoDuration } from "./iso-duration.js";
|
|
@@ -285,11 +285,11 @@ export class FlowEngine {
|
|
|
285
285
|
}
|
|
286
286
|
buildPerson(p) {
|
|
287
287
|
return { id: p.id, personnelClassIds: p.personnelClassIds, status: 'idle', taskId: null, window: p.window, ...(p.workCalendar ? { workCalendar: p.workCalendar } : {}), ...(p.homeLocation ? { location: p.homeLocation } : {}), ...(p.properties ? { properties: p.properties } : {}),
|
|
288
|
-
...(p.
|
|
288
|
+
...(p.testSpecificationIds ? { testSpecificationIds: p.testSpecificationIds } : {}), ...(p.testResults ? { testResults: p.testResults } : {}), ...effectiveOnly(p) };
|
|
289
289
|
}
|
|
290
290
|
buildAsset(a) {
|
|
291
291
|
return { id: a.id, assetClassIds: a.assetClassIds, location: a.homeLocation, status: 'idle', taskId: null, ...(a.properties ? { properties: a.properties } : {}),
|
|
292
|
-
...(a.
|
|
292
|
+
...(a.testSpecificationIds ? { testSpecificationIds: a.testSpecificationIds } : {}), ...(a.testResults ? { testResults: a.testResults } : {}), ...effectiveOnly(a) };
|
|
293
293
|
}
|
|
294
294
|
/**
|
|
295
295
|
* 이 커널이 **관측으로 구동된다**고 선언한다 — 미러가 첫 이벤트를 받기 전에도 그렇다.
|
|
@@ -369,7 +369,7 @@ export class FlowEngine {
|
|
|
369
369
|
};
|
|
370
370
|
}
|
|
371
371
|
buildEquipment(m) {
|
|
372
|
-
const eq = { id: m.id, kind: m.kind, location: m.homeLocation, homeLocation: m.homeLocation, ...(m.properties ? { properties: m.properties } : {}), ...(m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}), status: 'idle', taskId: null, runMs: 0, setupMs: 0, downMs: 0, goodCount: 0, scrapCount: 0, window: m.window, ...(m.workCalendar ? { workCalendar: m.workCalendar } : {}), ...effectiveOnly(m) };
|
|
372
|
+
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, scrapCount: 0, window: m.window, ...(m.workCalendar ? { workCalendar: m.workCalendar } : {}), ...effectiveOnly(m) };
|
|
373
373
|
if (m.mtbfMs !== undefined) {
|
|
374
374
|
eq.mtbfMs = m.mtbfMs;
|
|
375
375
|
eq.mttrMs = m.mttrMs;
|
|
@@ -384,7 +384,7 @@ export class FlowEngine {
|
|
|
384
384
|
addEquipment(m) {
|
|
385
385
|
if (this.equipment.has(m.id))
|
|
386
386
|
return;
|
|
387
|
-
const eq = { id: m.id, kind: m.kind, location: m.homeLocation, homeLocation: m.homeLocation, ...(m.properties ? { properties: m.properties } : {}), ...(m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}), status: 'idle', taskId: null, runMs: 0, setupMs: 0, downMs: 0, goodCount: 0, scrapCount: 0 };
|
|
387
|
+
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, scrapCount: 0 };
|
|
388
388
|
if (m.mtbfMs !== undefined) {
|
|
389
389
|
eq.mtbfMs = m.mtbfMs;
|
|
390
390
|
eq.mttrMs = m.mttrMs;
|
|
@@ -469,6 +469,8 @@ export class FlowEngine {
|
|
|
469
469
|
/* 관측 스냅샷이 들고 있는 것은 관측을 따른다(미러가 보드에서 읽어 실어 온다). */
|
|
470
470
|
...(m.properties ? { properties: m.properties } : {}),
|
|
471
471
|
...(m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}),
|
|
472
|
+
/* 결과도 이어받는다 — 잃으면 예측이 **자격 만료를 모르는 현장**에서 출발한다(낙관 쪽으로 치우친다). */
|
|
473
|
+
...(m.testResults ? { testResults: m.testResults } : {}),
|
|
472
474
|
...effectiveOnly(m)
|
|
473
475
|
});
|
|
474
476
|
}
|
|
@@ -483,6 +485,8 @@ export class FlowEngine {
|
|
|
483
485
|
...(p.location ? { location: p.location } : {}),
|
|
484
486
|
...(p.properties ? { properties: p.properties } : {}),
|
|
485
487
|
...(p.testSpecificationIds ? { testSpecificationIds: p.testSpecificationIds } : {}),
|
|
488
|
+
/* 결과도 이어받는다 — 잃으면 예측이 **자격 만료를 모르는 현장**에서 출발한다(낙관 쪽으로 치우친다). */
|
|
489
|
+
...(p.testResults ? { testResults: p.testResults } : {}),
|
|
486
490
|
...effectiveOnly(p)
|
|
487
491
|
});
|
|
488
492
|
}
|
|
@@ -494,6 +498,8 @@ export class FlowEngine {
|
|
|
494
498
|
id: a.id, assetClassIds: a.assetClassIds ?? prev?.assetClassIds, location: a.location, status: 'idle', taskId: null, carrying: a.carrying,
|
|
495
499
|
...(a.properties ? { properties: a.properties } : {}),
|
|
496
500
|
...(a.testSpecificationIds ? { testSpecificationIds: a.testSpecificationIds } : {}),
|
|
501
|
+
/* 결과도 이어받는다 — 잃으면 예측이 **자격 만료를 모르는 현장**에서 출발한다(낙관 쪽으로 치우친다). */
|
|
502
|
+
...(a.testResults ? { testResults: a.testResults } : {}),
|
|
497
503
|
...effectiveOnly(a)
|
|
498
504
|
});
|
|
499
505
|
}
|
|
@@ -812,7 +818,7 @@ export class FlowEngine {
|
|
|
812
818
|
}),
|
|
813
819
|
items: [...this.items.values()].map(i => this.itemState(i)),
|
|
814
820
|
equipment: [...this.equipment.values()].map(m => {
|
|
815
|
-
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 } : {}), origin: 'master', ...(this.offShift(m) ? { offShift: true, ...this.offReason(m) } : {}), ...this.effectivePart(m), ...(this.shiftOf(m) ? { shift: this.shiftOf(m) } : {}) };
|
|
821
|
+
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.kind ? [m.kind] : [], this.classDefs.equipment), origin: 'master', ...(this.offShift(m) ? { offShift: true, ...this.offReason(m) } : {}), ...this.effectivePart(m), ...(this.shiftOf(m) ? { shift: this.shiftOf(m) } : {}) };
|
|
816
822
|
const t = m.taskId ? this.tasks.get(m.taskId) : undefined;
|
|
817
823
|
if (t && t.status === 'in-progress' && t.intent !== 'process')
|
|
818
824
|
s.motion = { fromNode: t.fromNode, toNode: t.toNode, startedAtSimMs: this.clockMs - (t.durationMs - t.remainingMs), durationMs: t.durationMs, progress: this.progressOf(t), elapsedMs: t.durationMs - t.remainingMs };
|
|
@@ -826,7 +832,10 @@ export class FlowEngine {
|
|
|
826
832
|
st.properties = a.properties;
|
|
827
833
|
if (a.testSpecificationIds)
|
|
828
834
|
st.testSpecificationIds = a.testSpecificationIds;
|
|
835
|
+
if (a.testResults)
|
|
836
|
+
st.testResults = a.testResults;
|
|
829
837
|
Object.assign(st, this.effectivePart(a));
|
|
838
|
+
st.capability = this.capabilityOfResource(a, a.assetClassIds, this.classDefs.asset);
|
|
830
839
|
return st;
|
|
831
840
|
}),
|
|
832
841
|
persons: [...this.persons.values()].map(p => {
|
|
@@ -837,6 +846,10 @@ export class FlowEngine {
|
|
|
837
846
|
st.properties = p.properties;
|
|
838
847
|
if (p.testSpecificationIds)
|
|
839
848
|
st.testSpecificationIds = p.testSpecificationIds;
|
|
849
|
+
/* 결과도 함께 낸다 — 참조만 보내면 소비처는 "무엇으로 검증하는가" 까지만 알고 "지금 유효한가" 를
|
|
850
|
+
물을 수 없다(사실은 자기 집에서 나온다). 판정은 아래 `capability` 가 이미 답한다. */
|
|
851
|
+
if (p.testResults)
|
|
852
|
+
st.testResults = p.testResults;
|
|
840
853
|
if (this.personOffShift(p)) {
|
|
841
854
|
st.offShift = true;
|
|
842
855
|
Object.assign(st, this.offReason(p));
|
|
@@ -845,6 +858,7 @@ export class FlowEngine {
|
|
|
845
858
|
if (sh)
|
|
846
859
|
st.shift = sh;
|
|
847
860
|
Object.assign(st, this.effectivePart(p));
|
|
861
|
+
st.capability = this.capabilityOfResource(p, p.personnelClassIds, this.classDefs.personnel);
|
|
848
862
|
return st;
|
|
849
863
|
}),
|
|
850
864
|
/* 스냅샷이 **델타보다 가난하면 안 된다** — 예전에는 소요·남은 시간을 빼고 내보내서, 이 스냅샷으로
|
|
@@ -1567,9 +1581,8 @@ export class FlowEngine {
|
|
|
1567
1581
|
continue;
|
|
1568
1582
|
const avail = [...this.assets.values()].filter(
|
|
1569
1583
|
/* 인원과 같은 규칙 — 상속을 타고 닫아 판정하고 유효기간 밖은 제외한다. 요구는 등급 하나+수량. */
|
|
1570
|
-
a => a.
|
|
1584
|
+
a => this.capabilityOfResource(a, a.assetClassIds, this.classDefs.asset).available &&
|
|
1571
1585
|
!picked.includes(a.id) &&
|
|
1572
|
-
!this.outOfEffect(a) && // 폐기한 팔레트는 풀에서 빠진다
|
|
1573
1586
|
(req.assetClass === undefined ||
|
|
1574
1587
|
classClosure(a.assetClassIds, this.classDefs.asset, this.now()).has(req.assetClass)));
|
|
1575
1588
|
if (avail.length < want)
|
|
@@ -1854,16 +1867,24 @@ export class FlowEngine {
|
|
|
1854
1867
|
* 판정 규칙은 계약이 소유한다(`meetsTests`) — 여기서 다시 적으면 규칙이 두 곳이 되고, 한쪽이 만료를
|
|
1855
1868
|
* 조용히 통과시킨다.
|
|
1856
1869
|
*/
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1870
|
+
/**
|
|
1871
|
+
* 이 사람이 속한 등급들이 **요구하는 시험 목록** — 판정은 계약이 한다(`capabilityOf`).
|
|
1872
|
+
*
|
|
1873
|
+
* 상속을 타고 닫은 등급 전부의 요구를 모은다: 상위 등급이 요구하는 시험도 자격의 조건이다.
|
|
1874
|
+
*/
|
|
1875
|
+
/**
|
|
1876
|
+
* 자원 하나의 **가용 능력** — 계약의 판정에 이 커널의 시각·시간대·필수 시험을 채워 넘긴다.
|
|
1877
|
+
*
|
|
1878
|
+
* 배정도 스냅샷도 이 자리를 지난다. 예전에는 배정이 조건을 늘어놓고 화면이 플래그를 보고 짐작해서,
|
|
1879
|
+
* **자격이 만료된 사람이 화면에서는 `대기` 로 보였다**(놀고 있는 것은 맞지만 쓸 수 있는 것은 아니다).
|
|
1880
|
+
* 판정에 필요한 둘(시각·등급 상속)을 아는 것은 커널뿐이므로, 답도 커널이 낸다.
|
|
1881
|
+
*/
|
|
1882
|
+
capabilityOfResource(r, directClassIds, defs) {
|
|
1883
|
+
return capabilityOf(r, {
|
|
1884
|
+
at: this.now(),
|
|
1885
|
+
utcOffsetMinutes: this.boardDef?.utcOffsetMinutes,
|
|
1886
|
+
requiredTests: requiredTestsFor(directClassIds, defs, this.now())
|
|
1887
|
+
});
|
|
1867
1888
|
}
|
|
1868
1889
|
claimPersonnel(t) {
|
|
1869
1890
|
const need = this.operationSpecs.get(t.kind)?.personnelSpecification;
|
|
@@ -1874,24 +1895,19 @@ export class FlowEngine {
|
|
|
1874
1895
|
const want = Math.max(0, Math.floor(req.quantity ?? 0));
|
|
1875
1896
|
if (!want)
|
|
1876
1897
|
continue;
|
|
1877
|
-
const avail = [...this.persons.values()].filter(p => p.
|
|
1878
|
-
!picked.includes(p.id) &&
|
|
1879
|
-
!this.personOffShift(p) &&
|
|
1880
|
-
!this.outOfEffect(p) && // 입사 전·퇴사 후는 이 시각의 모델에 없다
|
|
1881
|
-
/* 자격은 **상속을 타고 닫아** 판정한다 — 사람은 여러 등급에 속할 수 있고(표준), 등급은
|
|
1882
|
-
다른 등급을 상속한다. "생산직 2명" 요구를 "용접 자격자" 가 만족해야 한다.
|
|
1883
|
-
유효기간 밖의 등급은 닫힘에서 빠진다(만료된 자격으로 배정되지 않는다). */
|
|
1884
|
-
(req.personnelClass === undefined ||
|
|
1885
|
-
classClosure(p.personnelClassIds, this.classDefs.personnel, this.now()).has(req.personnelClass)) &&
|
|
1898
|
+
const avail = [...this.persons.values()].filter(p => !picked.includes(p.id) &&
|
|
1886
1899
|
/*
|
|
1887
|
-
*
|
|
1888
|
-
*
|
|
1900
|
+
* **판정은 한 벌이다**(`capabilityOf`). 예전에는 이 자리에 조건을 늘어놓았고 화면은 같은
|
|
1901
|
+
* 판정을 자기 코드로 다시 만들었다 — 규칙이 두 벌이면 갈라지고, 배정은 막는데 화면은
|
|
1902
|
+
* "가용" 이라 말하는 순간이 온다(그 어긋남은 조용하다).
|
|
1889
1903
|
*
|
|
1890
|
-
*
|
|
1891
|
-
* 굶고, 원본이 결과를 주지 않는 현장에서는 그 제약이 선언되지 않은 것이다("선언한 것만
|
|
1892
|
-
* 제약이 된다" — 이 커널의 규율).
|
|
1904
|
+
* 요구된 시험은 등급에서 모아 넘긴다 — 등급 정의를 아는 것은 이쪽이다.
|
|
1893
1905
|
*/
|
|
1894
|
-
this.
|
|
1906
|
+
this.capabilityOfResource(p, p.personnelClassIds, this.classDefs.personnel).available &&
|
|
1907
|
+
/* 자격은 **상속을 타고 닫아** 판정한다 — 사람은 여러 등급에 속할 수 있고(표준), 등급은
|
|
1908
|
+
다른 등급을 상속한다. "생산직 2명" 요구를 "용접 자격자" 가 만족해야 한다. */
|
|
1909
|
+
(req.personnelClass === undefined ||
|
|
1910
|
+
classClosure(p.personnelClassIds, this.classDefs.personnel, this.now()).has(req.personnelClass)));
|
|
1895
1911
|
if (avail.length < want)
|
|
1896
1912
|
return null; // 한 등급이라도 모자라면 시작하지 않는다
|
|
1897
1913
|
for (let i = 0; i < want; i++)
|
|
@@ -1905,10 +1921,10 @@ export class FlowEngine {
|
|
|
1905
1921
|
* 여기서는 고르기만 한다 — 확정은 호출부가 다른 자원까지 확보한 뒤에 한다.
|
|
1906
1922
|
*/
|
|
1907
1923
|
claimEquipment(t) {
|
|
1908
|
-
const free = (kind, picked = []) => [...this.equipment.values()].filter(m =>
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1924
|
+
const free = (kind, picked = []) => [...this.equipment.values()].filter(m =>
|
|
1925
|
+
/* 사람과 **같은 판정**(§capabilityOfResource) — 예전에는 이 자리에 조건을 늘어놓아서
|
|
1926
|
+
설비의 검사 결과는 배정에서 아예 읽히지 않았다(사람만 자격을 봤다). */
|
|
1927
|
+
this.capabilityOfResource(m, m.kind ? [m.kind] : [], this.classDefs.equipment).available &&
|
|
1912
1928
|
!picked.includes(m.id) &&
|
|
1913
1929
|
/* 설비의 소속은 아직 단수(`kind`)다 — 저널에 기록이 쌓여 복수화를 별도 작업으로 두었다.
|
|
1914
1930
|
다만 등급 정의가 있으면 **상속은 지금도 탄다**: 'welder-6axis' 가 'welder' 를 상속하면
|
|
@@ -70,6 +70,13 @@ export declare class ObservedReducer {
|
|
|
70
70
|
* 갈라진다(적합성 하네스가 `mirrorOnly` 로 잡는다). 이것은 **판정의 입력**이고, 나가는 것은 판정뿐이다.
|
|
71
71
|
*/
|
|
72
72
|
private shifts;
|
|
73
|
+
/**
|
|
74
|
+
* 자원 **등급 정의** — 교대 선언과 같은 성격의 *판정 입력*이다(상태로 내보내지 않는다).
|
|
75
|
+
*
|
|
76
|
+
* 이것이 없으면 미러는 "이 사람에게 어떤 시험이 필수인가" 를 모른다. 시뮬은 막는데 미러는
|
|
77
|
+
* 통과시키는 어긋남이 생기고, 그 어긋남은 조용하다.
|
|
78
|
+
*/
|
|
79
|
+
private classDefs;
|
|
73
80
|
/** 시각 해석 기준(보드 선언) — 없으면 UTC. 캘린더의 `HH:MM` 이 어느 기준인지 정한다. */
|
|
74
81
|
private utcOffsetMinutes?;
|
|
75
82
|
constructor(board: TwinModelDef);
|
|
@@ -164,5 +171,12 @@ export declare class ObservedReducer {
|
|
|
164
171
|
private offShiftPart;
|
|
165
172
|
/** 유효 기간 밖이면 그 이유를 붙인다 — 미러의 시각 기준은 `observedAtMs`(마지막으로 들은 시점). */
|
|
166
173
|
private effectivityPart;
|
|
174
|
+
/**
|
|
175
|
+
* **왜 지금 쓰일 수 없나** — 시뮬과 **같은 함수**로 낸다(`capabilityOf`).
|
|
176
|
+
*
|
|
177
|
+
* 델타로 받지 않고 여기서 파생하는 이유는 `effectivity`·`shift` 와 같다: 자격 만료와 교대 경계는
|
|
178
|
+
* **이벤트 없이 시각만으로** 넘어간다. 델타로 받아 두면 만료된 자격이 영원히 유효하게 남는다.
|
|
179
|
+
*/
|
|
180
|
+
private capabilityPart;
|
|
167
181
|
snapshot(): ProjectedState;
|
|
168
182
|
}
|
package/dist/observed-reducer.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* - 운영 델타(task/equipment/order.status) → tasks·equipment·orders (EPCIS 로 재구성 불가한 절반)
|
|
20
20
|
* 마스터(로케이션)는 board 초기화 + applyMaster 로 갱신(마스터 동기).
|
|
21
21
|
*/
|
|
22
|
-
import { OP_EVENT, locationStatusOf, readBoardEquipment, readBoardLocations, readBoardAssets, effectivityAt, offCalendarAt, offCalendarReasonAt, activeShiftAt } from "./contract.js";
|
|
22
|
+
import { OP_EVENT, capabilityOf, requiredTestsFor, locationStatusOf, readBoardEquipment, readBoardLocations, readBoardAssets, effectivityAt, offCalendarAt, offCalendarReasonAt, activeShiftAt } from "./contract.js";
|
|
23
23
|
import { ILMD_ATTR, parseEpc } from "./epcis.js";
|
|
24
24
|
/**
|
|
25
25
|
* 투영이 들고 있는 물품 — **계약(ItemState)을 축소하지 않는다.**
|
|
@@ -62,18 +62,26 @@ export class ObservedReducer {
|
|
|
62
62
|
* 갈라진다(적합성 하네스가 `mirrorOnly` 로 잡는다). 이것은 **판정의 입력**이고, 나가는 것은 판정뿐이다.
|
|
63
63
|
*/
|
|
64
64
|
shifts = new Map();
|
|
65
|
+
/**
|
|
66
|
+
* 자원 **등급 정의** — 교대 선언과 같은 성격의 *판정 입력*이다(상태로 내보내지 않는다).
|
|
67
|
+
*
|
|
68
|
+
* 이것이 없으면 미러는 "이 사람에게 어떤 시험이 필수인가" 를 모른다. 시뮬은 막는데 미러는
|
|
69
|
+
* 통과시키는 어긋남이 생기고, 그 어긋남은 조용하다.
|
|
70
|
+
*/
|
|
71
|
+
classDefs = {};
|
|
65
72
|
/** 시각 해석 기준(보드 선언) — 없으면 UTC. 캘린더의 `HH:MM` 이 어느 기준인지 정한다. */
|
|
66
73
|
utcOffsetMinutes;
|
|
67
74
|
constructor(board) {
|
|
68
75
|
this.utcOffsetMinutes = board.utcOffsetMinutes;
|
|
76
|
+
this.classDefs = { personnel: board.personnelClasses, equipment: board.equipmentClasses, asset: board.assetClasses };
|
|
69
77
|
for (const n of readBoardLocations(board))
|
|
70
78
|
this.master.set(n.id, { id: n.id, type: n.type, capacity: n.capacity, parallelism: n.parallelism, parentId: n.parentId, origin: 'master' });
|
|
71
79
|
// 설비 기준선(마스터) — equipment.status 델타로 갱신됨.
|
|
72
80
|
for (const m of readBoardEquipment(board))
|
|
73
|
-
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 } : {}), ...effectiveOf(m), origin: 'master' });
|
|
81
|
+
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' });
|
|
74
82
|
// 사람 기준선(마스터) — person.status 델타로 갱신됨.
|
|
75
83
|
for (const p of board.persons ?? [])
|
|
76
|
-
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 } : {}), ...effectiveOf(p) });
|
|
84
|
+
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) });
|
|
77
85
|
/* 교대 선언은 **미러도 들고 있어야 한다** — 설비 델타에는 `offShift` 자리조차 없어서, 그것 없이는
|
|
78
86
|
미러의 설비가 점심 휴게 중에도 "대기" 로 보였다. */
|
|
79
87
|
for (const m of readBoardEquipment(board))
|
|
@@ -81,7 +89,7 @@ export class ObservedReducer {
|
|
|
81
89
|
for (const p of board.persons ?? [])
|
|
82
90
|
this.rememberShift(`person:${p.id}`, p);
|
|
83
91
|
for (const a of readBoardAssets(board))
|
|
84
|
-
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 } : {}), ...effectiveOf(a) });
|
|
92
|
+
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) });
|
|
85
93
|
}
|
|
86
94
|
/**
|
|
87
95
|
* **구조를 갈아탄다** — 관측된 사실은 지키고 토폴로지만 새 선언으로 바꾼다.
|
|
@@ -138,17 +146,17 @@ export class ObservedReducer {
|
|
|
138
146
|
const assetsDropped = dropMissing(this.assets, assets);
|
|
139
147
|
for (const m of equipment) {
|
|
140
148
|
if (!this.equipment.has(m.id))
|
|
141
|
-
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 } : {}), ...effectiveOf(m), origin: 'master' });
|
|
149
|
+
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' });
|
|
142
150
|
this.rememberShift(`eq:${m.id}`, m);
|
|
143
151
|
}
|
|
144
152
|
for (const p of persons) {
|
|
145
153
|
if (!this.persons.has(p.id))
|
|
146
|
-
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 } : {}), ...effectiveOf(p) });
|
|
154
|
+
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) });
|
|
147
155
|
this.rememberShift(`person:${p.id}`, p);
|
|
148
156
|
}
|
|
149
157
|
for (const a of assets)
|
|
150
158
|
if (!this.assets.has(a.id))
|
|
151
|
-
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 } : {}), ...effectiveOf(a) });
|
|
159
|
+
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) });
|
|
152
160
|
return { locationsAdded, locationsDropped, equipmentDropped, personsDropped, assetsDropped };
|
|
153
161
|
}
|
|
154
162
|
/** 마스터 동기 — 로케이션 추가/변경/제거. */
|
|
@@ -571,6 +579,19 @@ export class ObservedReducer {
|
|
|
571
579
|
const e = effectivityAt(r, new Date(this.observedAtMs).toISOString());
|
|
572
580
|
return e ? { effectivity: e } : {};
|
|
573
581
|
}
|
|
582
|
+
/**
|
|
583
|
+
* **왜 지금 쓰일 수 없나** — 시뮬과 **같은 함수**로 낸다(`capabilityOf`).
|
|
584
|
+
*
|
|
585
|
+
* 델타로 받지 않고 여기서 파생하는 이유는 `effectivity`·`shift` 와 같다: 자격 만료와 교대 경계는
|
|
586
|
+
* **이벤트 없이 시각만으로** 넘어간다. 델타로 받아 두면 만료된 자격이 영원히 유효하게 남는다.
|
|
587
|
+
*/
|
|
588
|
+
capabilityPart(r, key, directClassIds, defs) {
|
|
589
|
+
const at = this.observedAtMs === undefined ? undefined : new Date(this.observedAtMs).toISOString();
|
|
590
|
+
const decl = this.shifts.get(key);
|
|
591
|
+
return {
|
|
592
|
+
capability: capabilityOf({ ...r, ...(decl?.window ? { window: decl.window } : {}), ...(decl?.workCalendar ? { workCalendar: decl.workCalendar } : {}) }, { at, utcOffsetMinutes: this.utcOffsetMinutes, requiredTests: requiredTestsFor(directClassIds, defs, at) })
|
|
593
|
+
};
|
|
594
|
+
}
|
|
574
595
|
snapshot() {
|
|
575
596
|
const occ = new Map();
|
|
576
597
|
for (const it of this.items.values())
|
|
@@ -595,10 +616,10 @@ export class ObservedReducer {
|
|
|
595
616
|
items: [...this.items.values()].map(i => ({ ...i })),
|
|
596
617
|
/* 유효 기간 판정은 **저장하지 않고 여기서 낸다** — 시뮬과 **같은 함수**(`effectivityAt`)를 부른다.
|
|
597
618
|
만료는 이벤트 없이 시각만으로 일어나므로, 델타로 받아 두면 유휴 자원이 영원히 유효하게 남는다. */
|
|
598
|
-
persons: [...this.persons.values()].map(p => ({ ...p, ...this.effectivityPart(p), ...this.offShiftPart(`person:${p.id}`) })),
|
|
599
|
-
assets: [...this.assets.values()].map(a => ({ ...a, ...this.effectivityPart(a) })),
|
|
619
|
+
persons: [...this.persons.values()].map(p => ({ ...p, ...this.effectivityPart(p), ...this.offShiftPart(`person:${p.id}`), ...this.capabilityPart(p, `person:${p.id}`, p.personnelClassIds, this.classDefs.personnel) })),
|
|
620
|
+
assets: [...this.assets.values()].map(a => ({ ...a, ...this.effectivityPart(a), ...this.capabilityPart(a, `asset:${a.id}`, a.assetClassIds, this.classDefs.asset) })),
|
|
600
621
|
tasks: [...this.tasks.values()].map(t => ({ ...t })),
|
|
601
|
-
equipment: [...this.equipment.values()].map(m => ({ ...m, ...this.effectivityPart(m), ...this.offShiftPart(`eq:${m.id}`) })),
|
|
622
|
+
equipment: [...this.equipment.values()].map(m => ({ ...m, ...this.effectivityPart(m), ...this.offShiftPart(`eq:${m.id}`), ...this.capabilityPart(m, `eq:${m.id}`, m.kind ? [m.kind] : [], this.classDefs.equipment) })),
|
|
602
623
|
orders: [...this.orders.values()].map(o => ({ ...o })),
|
|
603
624
|
acked: [...this.acked]
|
|
604
625
|
};
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -73,6 +73,7 @@ __export(index_exports, {
|
|
|
73
73
|
axisInfo: () => axisInfo,
|
|
74
74
|
axisSource: () => axisSource,
|
|
75
75
|
capabilitiesForType: () => capabilitiesForType,
|
|
76
|
+
capabilityOf: () => capabilityOf,
|
|
76
77
|
classClosure: () => classClosure,
|
|
77
78
|
compareStates: () => compareStates,
|
|
78
79
|
computeOee: () => computeOee,
|
|
@@ -118,6 +119,7 @@ __export(index_exports, {
|
|
|
118
119
|
relationsTo: () => relationsTo,
|
|
119
120
|
replay: () => replay,
|
|
120
121
|
replaySegments: () => replaySegments,
|
|
122
|
+
requiredTestsFor: () => requiredTestsFor,
|
|
121
123
|
retiredVocabularyIn: () => retiredVocabularyIn,
|
|
122
124
|
sgtinClass: () => sgtinClass,
|
|
123
125
|
sgtinUri: () => sgtinUri,
|
|
@@ -409,6 +411,33 @@ function offCalendarAt(r, ms2, utcOffsetMinutes) {
|
|
|
409
411
|
const h = Math.floor(minute / 60);
|
|
410
412
|
return !(w.startHour <= w.endHour ? h >= w.startHour && h < w.endHour : h >= w.startHour || h < w.endHour);
|
|
411
413
|
}
|
|
414
|
+
function requiredTestsFor(directIds, defs, at) {
|
|
415
|
+
if (!defs?.length) return [];
|
|
416
|
+
const closure = classClosure(directIds, defs, at);
|
|
417
|
+
const required = [];
|
|
418
|
+
for (const d of defs) if (closure.has(d.id)) required.push(...d.testSpecificationIds ?? []);
|
|
419
|
+
return required;
|
|
420
|
+
}
|
|
421
|
+
function capabilityOf(r, ctx) {
|
|
422
|
+
const at = ctx?.at;
|
|
423
|
+
const eff = effectivityAt(r, at);
|
|
424
|
+
if (eff === "not-yet") return { available: false, reason: "not-yet" };
|
|
425
|
+
if (eff === "expired") return { available: false, reason: "retired" };
|
|
426
|
+
if (r.held) return { available: false, reason: "held" };
|
|
427
|
+
if (r.status === "down") return { available: false, reason: "down" };
|
|
428
|
+
if (at) {
|
|
429
|
+
const ms2 = Date.parse(at);
|
|
430
|
+
if (Number.isFinite(ms2)) {
|
|
431
|
+
const why = offCalendarReasonAt(r, ms2, ctx?.utcOffsetMinutes);
|
|
432
|
+
if (why === "non-working") return { available: false, reason: "resting" };
|
|
433
|
+
if (why === "off-hours") return { available: false, reason: "off-shift" };
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (ctx?.requiredTests?.length && !meetsTests(ctx.requiredTests, r.testResults, at))
|
|
437
|
+
return { available: false, reason: "test-expired" };
|
|
438
|
+
if (r.status && r.status !== "idle" && r.status !== "available") return { available: false, reason: "working" };
|
|
439
|
+
return { available: true, reason: "available" };
|
|
440
|
+
}
|
|
412
441
|
var OP_EVENT = {
|
|
413
442
|
task: "task.status",
|
|
414
443
|
equipment: "equipment.status",
|
|
@@ -954,16 +983,24 @@ var ObservedReducer = class {
|
|
|
954
983
|
* 갈라진다(적합성 하네스가 `mirrorOnly` 로 잡는다). 이것은 **판정의 입력**이고, 나가는 것은 판정뿐이다.
|
|
955
984
|
*/
|
|
956
985
|
shifts = /* @__PURE__ */ new Map();
|
|
986
|
+
/**
|
|
987
|
+
* 자원 **등급 정의** — 교대 선언과 같은 성격의 *판정 입력*이다(상태로 내보내지 않는다).
|
|
988
|
+
*
|
|
989
|
+
* 이것이 없으면 미러는 "이 사람에게 어떤 시험이 필수인가" 를 모른다. 시뮬은 막는데 미러는
|
|
990
|
+
* 통과시키는 어긋남이 생기고, 그 어긋남은 조용하다.
|
|
991
|
+
*/
|
|
992
|
+
classDefs = {};
|
|
957
993
|
/** 시각 해석 기준(보드 선언) — 없으면 UTC. 캘린더의 `HH:MM` 이 어느 기준인지 정한다. */
|
|
958
994
|
utcOffsetMinutes;
|
|
959
995
|
constructor(board) {
|
|
960
996
|
this.utcOffsetMinutes = board.utcOffsetMinutes;
|
|
997
|
+
this.classDefs = { personnel: board.personnelClasses, equipment: board.equipmentClasses, asset: board.assetClasses };
|
|
961
998
|
for (const n of readBoardLocations(board)) this.master.set(n.id, { id: n.id, type: n.type, capacity: n.capacity, parallelism: n.parallelism, parentId: n.parentId, origin: "master" });
|
|
962
|
-
for (const m of readBoardEquipment(board)) 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 } : {}, ...effectiveOf(m), origin: "master" });
|
|
963
|
-
for (const p of board.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 } : {}, ...effectiveOf(p) });
|
|
999
|
+
for (const m of readBoardEquipment(board)) 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" });
|
|
1000
|
+
for (const p of board.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) });
|
|
964
1001
|
for (const m of readBoardEquipment(board)) this.rememberShift(`eq:${m.id}`, m);
|
|
965
1002
|
for (const p of board.persons ?? []) this.rememberShift(`person:${p.id}`, p);
|
|
966
|
-
for (const a of readBoardAssets(board)) 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 } : {}, ...effectiveOf(a) });
|
|
1003
|
+
for (const a of readBoardAssets(board)) 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) });
|
|
967
1004
|
}
|
|
968
1005
|
/**
|
|
969
1006
|
* **구조를 갈아탄다** — 관측된 사실은 지키고 토폴로지만 새 선언으로 바꾼다.
|
|
@@ -1015,17 +1052,17 @@ var ObservedReducer = class {
|
|
|
1015
1052
|
const assetsDropped = dropMissing(this.assets, assets);
|
|
1016
1053
|
for (const m of equipment) {
|
|
1017
1054
|
if (!this.equipment.has(m.id))
|
|
1018
|
-
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 } : {}, ...effectiveOf(m), origin: "master" });
|
|
1055
|
+
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" });
|
|
1019
1056
|
this.rememberShift(`eq:${m.id}`, m);
|
|
1020
1057
|
}
|
|
1021
1058
|
for (const p of persons) {
|
|
1022
1059
|
if (!this.persons.has(p.id))
|
|
1023
|
-
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 } : {}, ...effectiveOf(p) });
|
|
1060
|
+
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) });
|
|
1024
1061
|
this.rememberShift(`person:${p.id}`, p);
|
|
1025
1062
|
}
|
|
1026
1063
|
for (const a of assets)
|
|
1027
1064
|
if (!this.assets.has(a.id))
|
|
1028
|
-
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 } : {}, ...effectiveOf(a) });
|
|
1065
|
+
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) });
|
|
1029
1066
|
return { locationsAdded, locationsDropped, equipmentDropped, personsDropped, assetsDropped };
|
|
1030
1067
|
}
|
|
1031
1068
|
/** 마스터 동기 — 로케이션 추가/변경/제거. */
|
|
@@ -1378,6 +1415,22 @@ var ObservedReducer = class {
|
|
|
1378
1415
|
const e = effectivityAt(r, new Date(this.observedAtMs).toISOString());
|
|
1379
1416
|
return e ? { effectivity: e } : {};
|
|
1380
1417
|
}
|
|
1418
|
+
/**
|
|
1419
|
+
* **왜 지금 쓰일 수 없나** — 시뮬과 **같은 함수**로 낸다(`capabilityOf`).
|
|
1420
|
+
*
|
|
1421
|
+
* 델타로 받지 않고 여기서 파생하는 이유는 `effectivity`·`shift` 와 같다: 자격 만료와 교대 경계는
|
|
1422
|
+
* **이벤트 없이 시각만으로** 넘어간다. 델타로 받아 두면 만료된 자격이 영원히 유효하게 남는다.
|
|
1423
|
+
*/
|
|
1424
|
+
capabilityPart(r, key, directClassIds, defs) {
|
|
1425
|
+
const at = this.observedAtMs === void 0 ? void 0 : new Date(this.observedAtMs).toISOString();
|
|
1426
|
+
const decl = this.shifts.get(key);
|
|
1427
|
+
return {
|
|
1428
|
+
capability: capabilityOf(
|
|
1429
|
+
{ ...r, ...decl?.window ? { window: decl.window } : {}, ...decl?.workCalendar ? { workCalendar: decl.workCalendar } : {} },
|
|
1430
|
+
{ at, utcOffsetMinutes: this.utcOffsetMinutes, requiredTests: requiredTestsFor(directClassIds, defs, at) }
|
|
1431
|
+
)
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1381
1434
|
snapshot() {
|
|
1382
1435
|
const occ = /* @__PURE__ */ new Map();
|
|
1383
1436
|
for (const it of this.items.values()) occ.set(it.location, (occ.get(it.location) ?? 0) + 1);
|
|
@@ -1403,10 +1456,10 @@ var ObservedReducer = class {
|
|
|
1403
1456
|
items: [...this.items.values()].map((i) => ({ ...i })),
|
|
1404
1457
|
/* 유효 기간 판정은 **저장하지 않고 여기서 낸다** — 시뮬과 **같은 함수**(`effectivityAt`)를 부른다.
|
|
1405
1458
|
만료는 이벤트 없이 시각만으로 일어나므로, 델타로 받아 두면 유휴 자원이 영원히 유효하게 남는다. */
|
|
1406
|
-
persons: [...this.persons.values()].map((p) => ({ ...p, ...this.effectivityPart(p), ...this.offShiftPart(`person:${p.id}`) })),
|
|
1407
|
-
assets: [...this.assets.values()].map((a) => ({ ...a, ...this.effectivityPart(a) })),
|
|
1459
|
+
persons: [...this.persons.values()].map((p) => ({ ...p, ...this.effectivityPart(p), ...this.offShiftPart(`person:${p.id}`), ...this.capabilityPart(p, `person:${p.id}`, p.personnelClassIds, this.classDefs.personnel) })),
|
|
1460
|
+
assets: [...this.assets.values()].map((a) => ({ ...a, ...this.effectivityPart(a), ...this.capabilityPart(a, `asset:${a.id}`, a.assetClassIds, this.classDefs.asset) })),
|
|
1408
1461
|
tasks: [...this.tasks.values()].map((t) => ({ ...t })),
|
|
1409
|
-
equipment: [...this.equipment.values()].map((m) => ({ ...m, ...this.effectivityPart(m), ...this.offShiftPart(`eq:${m.id}`) })),
|
|
1462
|
+
equipment: [...this.equipment.values()].map((m) => ({ ...m, ...this.effectivityPart(m), ...this.offShiftPart(`eq:${m.id}`), ...this.capabilityPart(m, `eq:${m.id}`, m.kind ? [m.kind] : [], this.classDefs.equipment) })),
|
|
1410
1463
|
orders: [...this.orders.values()].map((o) => ({ ...o })),
|
|
1411
1464
|
acked: [...this.acked]
|
|
1412
1465
|
};
|
|
@@ -2420,7 +2473,6 @@ var FlowEngine = class {
|
|
|
2420
2473
|
...p.workCalendar ? { workCalendar: p.workCalendar } : {},
|
|
2421
2474
|
...p.homeLocation ? { location: p.homeLocation } : {},
|
|
2422
2475
|
...p.properties ? { properties: p.properties } : {},
|
|
2423
|
-
...p.testResults ? { testResults: p.testResults } : {},
|
|
2424
2476
|
...p.testSpecificationIds ? { testSpecificationIds: p.testSpecificationIds } : {},
|
|
2425
2477
|
...p.testResults ? { testResults: p.testResults } : {},
|
|
2426
2478
|
...effectiveOnly(p)
|
|
@@ -2434,7 +2486,6 @@ var FlowEngine = class {
|
|
|
2434
2486
|
status: "idle",
|
|
2435
2487
|
taskId: null,
|
|
2436
2488
|
...a.properties ? { properties: a.properties } : {},
|
|
2437
|
-
...a.testResults ? { testResults: a.testResults } : {},
|
|
2438
2489
|
...a.testSpecificationIds ? { testSpecificationIds: a.testSpecificationIds } : {},
|
|
2439
2490
|
...a.testResults ? { testResults: a.testResults } : {},
|
|
2440
2491
|
...effectiveOnly(a)
|
|
@@ -2504,7 +2555,7 @@ var FlowEngine = class {
|
|
|
2504
2555
|
};
|
|
2505
2556
|
}
|
|
2506
2557
|
buildEquipment(m) {
|
|
2507
|
-
const eq = { id: m.id, kind: m.kind, location: m.homeLocation, homeLocation: m.homeLocation, ...m.properties ? { properties: m.properties } : {}, ...m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}, status: "idle", taskId: null, runMs: 0, setupMs: 0, downMs: 0, goodCount: 0, scrapCount: 0, window: m.window, ...m.workCalendar ? { workCalendar: m.workCalendar } : {}, ...effectiveOnly(m) };
|
|
2558
|
+
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, scrapCount: 0, window: m.window, ...m.workCalendar ? { workCalendar: m.workCalendar } : {}, ...effectiveOnly(m) };
|
|
2508
2559
|
if (m.mtbfMs !== void 0) {
|
|
2509
2560
|
eq.mtbfMs = m.mtbfMs;
|
|
2510
2561
|
eq.mttrMs = m.mttrMs;
|
|
@@ -2518,7 +2569,7 @@ var FlowEngine = class {
|
|
|
2518
2569
|
*/
|
|
2519
2570
|
addEquipment(m) {
|
|
2520
2571
|
if (this.equipment.has(m.id)) return;
|
|
2521
|
-
const eq = { id: m.id, kind: m.kind, location: m.homeLocation, homeLocation: m.homeLocation, ...m.properties ? { properties: m.properties } : {}, ...m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {}, status: "idle", taskId: null, runMs: 0, setupMs: 0, downMs: 0, goodCount: 0, scrapCount: 0 };
|
|
2572
|
+
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, scrapCount: 0 };
|
|
2522
2573
|
if (m.mtbfMs !== void 0) {
|
|
2523
2574
|
eq.mtbfMs = m.mtbfMs;
|
|
2524
2575
|
eq.mttrMs = m.mttrMs;
|
|
@@ -2594,6 +2645,8 @@ var FlowEngine = class {
|
|
|
2594
2645
|
/* 관측 스냅샷이 들고 있는 것은 관측을 따른다(미러가 보드에서 읽어 실어 온다). */
|
|
2595
2646
|
...m.properties ? { properties: m.properties } : {},
|
|
2596
2647
|
...m.testSpecificationIds ? { testSpecificationIds: m.testSpecificationIds } : {},
|
|
2648
|
+
/* 결과도 이어받는다 — 잃으면 예측이 **자격 만료를 모르는 현장**에서 출발한다(낙관 쪽으로 치우친다). */
|
|
2649
|
+
...m.testResults ? { testResults: m.testResults } : {},
|
|
2597
2650
|
...effectiveOnly(m)
|
|
2598
2651
|
});
|
|
2599
2652
|
}
|
|
@@ -2608,6 +2661,8 @@ var FlowEngine = class {
|
|
|
2608
2661
|
...p.location ? { location: p.location } : {},
|
|
2609
2662
|
...p.properties ? { properties: p.properties } : {},
|
|
2610
2663
|
...p.testSpecificationIds ? { testSpecificationIds: p.testSpecificationIds } : {},
|
|
2664
|
+
/* 결과도 이어받는다 — 잃으면 예측이 **자격 만료를 모르는 현장**에서 출발한다(낙관 쪽으로 치우친다). */
|
|
2665
|
+
...p.testResults ? { testResults: p.testResults } : {},
|
|
2611
2666
|
...effectiveOnly(p)
|
|
2612
2667
|
});
|
|
2613
2668
|
}
|
|
@@ -2623,6 +2678,8 @@ var FlowEngine = class {
|
|
|
2623
2678
|
carrying: a.carrying,
|
|
2624
2679
|
...a.properties ? { properties: a.properties } : {},
|
|
2625
2680
|
...a.testSpecificationIds ? { testSpecificationIds: a.testSpecificationIds } : {},
|
|
2681
|
+
/* 결과도 이어받는다 — 잃으면 예측이 **자격 만료를 모르는 현장**에서 출발한다(낙관 쪽으로 치우친다). */
|
|
2682
|
+
...a.testResults ? { testResults: a.testResults } : {},
|
|
2626
2683
|
...effectiveOnly(a)
|
|
2627
2684
|
});
|
|
2628
2685
|
}
|
|
@@ -2908,7 +2965,7 @@ var FlowEngine = class {
|
|
|
2908
2965
|
}),
|
|
2909
2966
|
items: [...this.items.values()].map((i) => this.itemState(i)),
|
|
2910
2967
|
equipment: [...this.equipment.values()].map((m) => {
|
|
2911
|
-
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 } : {}, origin: "master", ...this.offShift(m) ? { offShift: true, ...this.offReason(m) } : {}, ...this.effectivePart(m), ...this.shiftOf(m) ? { shift: this.shiftOf(m) } : {} };
|
|
2968
|
+
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.kind ? [m.kind] : [], this.classDefs.equipment), origin: "master", ...this.offShift(m) ? { offShift: true, ...this.offReason(m) } : {}, ...this.effectivePart(m), ...this.shiftOf(m) ? { shift: this.shiftOf(m) } : {} };
|
|
2912
2969
|
const t = m.taskId ? this.tasks.get(m.taskId) : void 0;
|
|
2913
2970
|
if (t && t.status === "in-progress" && t.intent !== "process") s.motion = { fromNode: t.fromNode, toNode: t.toNode, startedAtSimMs: this.clockMs - (t.durationMs - t.remainingMs), durationMs: t.durationMs, progress: this.progressOf(t), elapsedMs: t.durationMs - t.remainingMs };
|
|
2914
2971
|
return s;
|
|
@@ -2918,7 +2975,9 @@ var FlowEngine = class {
|
|
|
2918
2975
|
if (a.carrying) st.carrying = a.carrying;
|
|
2919
2976
|
if (a.properties) st.properties = a.properties;
|
|
2920
2977
|
if (a.testSpecificationIds) st.testSpecificationIds = a.testSpecificationIds;
|
|
2978
|
+
if (a.testResults) st.testResults = a.testResults;
|
|
2921
2979
|
Object.assign(st, this.effectivePart(a));
|
|
2980
|
+
st.capability = this.capabilityOfResource(a, a.assetClassIds, this.classDefs.asset);
|
|
2922
2981
|
return st;
|
|
2923
2982
|
}),
|
|
2924
2983
|
persons: [...this.persons.values()].map((p) => {
|
|
@@ -2926,6 +2985,7 @@ var FlowEngine = class {
|
|
|
2926
2985
|
if (p.location) st.location = p.location;
|
|
2927
2986
|
if (p.properties) st.properties = p.properties;
|
|
2928
2987
|
if (p.testSpecificationIds) st.testSpecificationIds = p.testSpecificationIds;
|
|
2988
|
+
if (p.testResults) st.testResults = p.testResults;
|
|
2929
2989
|
if (this.personOffShift(p)) {
|
|
2930
2990
|
st.offShift = true;
|
|
2931
2991
|
Object.assign(st, this.offReason(p));
|
|
@@ -2933,6 +2993,7 @@ var FlowEngine = class {
|
|
|
2933
2993
|
const sh = this.shiftOf(p);
|
|
2934
2994
|
if (sh) st.shift = sh;
|
|
2935
2995
|
Object.assign(st, this.effectivePart(p));
|
|
2996
|
+
st.capability = this.capabilityOfResource(p, p.personnelClassIds, this.classDefs.personnel);
|
|
2936
2997
|
return st;
|
|
2937
2998
|
}),
|
|
2938
2999
|
/* 스냅샷이 **델타보다 가난하면 안 된다** — 예전에는 소요·남은 시간을 빼고 내보내서, 이 스냅샷으로
|
|
@@ -3602,8 +3663,7 @@ var FlowEngine = class {
|
|
|
3602
3663
|
if (!want) continue;
|
|
3603
3664
|
const avail = [...this.assets.values()].filter(
|
|
3604
3665
|
/* 인원과 같은 규칙 — 상속을 타고 닫아 판정하고 유효기간 밖은 제외한다. 요구는 등급 하나+수량. */
|
|
3605
|
-
(a) => a.
|
|
3606
|
-
(req.assetClass === void 0 || classClosure(a.assetClassIds, this.classDefs.asset, this.now()).has(req.assetClass))
|
|
3666
|
+
(a) => this.capabilityOfResource(a, a.assetClassIds, this.classDefs.asset).available && !picked.includes(a.id) && (req.assetClass === void 0 || classClosure(a.assetClassIds, this.classDefs.asset, this.now()).has(req.assetClass))
|
|
3607
3667
|
);
|
|
3608
3668
|
if (avail.length < want) return null;
|
|
3609
3669
|
for (let i = 0; i < want; i++) picked.push(avail[i].id);
|
|
@@ -3849,13 +3909,24 @@ var FlowEngine = class {
|
|
|
3849
3909
|
* 판정 규칙은 계약이 소유한다(`meetsTests`) — 여기서 다시 적으면 규칙이 두 곳이 되고, 한쪽이 만료를
|
|
3850
3910
|
* 조용히 통과시킨다.
|
|
3851
3911
|
*/
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3912
|
+
/**
|
|
3913
|
+
* 이 사람이 속한 등급들이 **요구하는 시험 목록** — 판정은 계약이 한다(`capabilityOf`).
|
|
3914
|
+
*
|
|
3915
|
+
* 상속을 타고 닫은 등급 전부의 요구를 모은다: 상위 등급이 요구하는 시험도 자격의 조건이다.
|
|
3916
|
+
*/
|
|
3917
|
+
/**
|
|
3918
|
+
* 자원 하나의 **가용 능력** — 계약의 판정에 이 커널의 시각·시간대·필수 시험을 채워 넘긴다.
|
|
3919
|
+
*
|
|
3920
|
+
* 배정도 스냅샷도 이 자리를 지난다. 예전에는 배정이 조건을 늘어놓고 화면이 플래그를 보고 짐작해서,
|
|
3921
|
+
* **자격이 만료된 사람이 화면에서는 `대기` 로 보였다**(놀고 있는 것은 맞지만 쓸 수 있는 것은 아니다).
|
|
3922
|
+
* 판정에 필요한 둘(시각·등급 상속)을 아는 것은 커널뿐이므로, 답도 커널이 낸다.
|
|
3923
|
+
*/
|
|
3924
|
+
capabilityOfResource(r, directClassIds, defs) {
|
|
3925
|
+
return capabilityOf(r, {
|
|
3926
|
+
at: this.now(),
|
|
3927
|
+
utcOffsetMinutes: this.boardDef?.utcOffsetMinutes,
|
|
3928
|
+
requiredTests: requiredTestsFor(directClassIds, defs, this.now())
|
|
3929
|
+
});
|
|
3859
3930
|
}
|
|
3860
3931
|
claimPersonnel(t) {
|
|
3861
3932
|
const need = this.operationSpecs.get(t.kind)?.personnelSpecification;
|
|
@@ -3865,19 +3936,16 @@ var FlowEngine = class {
|
|
|
3865
3936
|
const want = Math.max(0, Math.floor(req.quantity ?? 0));
|
|
3866
3937
|
if (!want) continue;
|
|
3867
3938
|
const avail = [...this.persons.values()].filter(
|
|
3868
|
-
(p) =>
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
(req.personnelClass === void 0 || classClosure(p.personnelClassIds, this.classDefs.personnel, this.now()).has(req.personnelClass)) && /*
|
|
3873
|
-
* **요구된 시험을 만족하나** — 등급이 요구하고(`testSpecificationIds`) 사람이 결과를 든다
|
|
3874
|
-
* (`testResults`). 만료·불합격이면 그 자격은 성립하지 않는다.
|
|
3939
|
+
(p) => !picked.includes(p.id) && /*
|
|
3940
|
+
* **판정은 한 벌이다**(`capabilityOf`). 예전에는 이 자리에 조건을 늘어놓았고 화면은 같은
|
|
3941
|
+
* 판정을 자기 코드로 다시 만들었다 — 규칙이 두 벌이면 갈라지고, 배정은 막는데 화면은
|
|
3942
|
+
* "가용" 이라 말하는 순간이 온다(그 어긋남은 조용하다).
|
|
3875
3943
|
*
|
|
3876
|
-
*
|
|
3877
|
-
* 굶고, 원본이 결과를 주지 않는 현장에서는 그 제약이 선언되지 않은 것이다("선언한 것만
|
|
3878
|
-
* 제약이 된다" — 이 커널의 규율).
|
|
3944
|
+
* 요구된 시험은 등급에서 모아 넘긴다 — 등급 정의를 아는 것은 이쪽이다.
|
|
3879
3945
|
*/
|
|
3880
|
-
this.
|
|
3946
|
+
this.capabilityOfResource(p, p.personnelClassIds, this.classDefs.personnel).available && /* 자격은 **상속을 타고 닫아** 판정한다 — 사람은 여러 등급에 속할 수 있고(표준), 등급은
|
|
3947
|
+
다른 등급을 상속한다. "생산직 2명" 요구를 "용접 자격자" 가 만족해야 한다. */
|
|
3948
|
+
(req.personnelClass === void 0 || classClosure(p.personnelClassIds, this.classDefs.personnel, this.now()).has(req.personnelClass))
|
|
3881
3949
|
);
|
|
3882
3950
|
if (avail.length < want) return null;
|
|
3883
3951
|
for (let i = 0; i < want; i++) picked.push(avail[i].id);
|
|
@@ -3891,11 +3959,14 @@ var FlowEngine = class {
|
|
|
3891
3959
|
*/
|
|
3892
3960
|
claimEquipment(t) {
|
|
3893
3961
|
const free = (kind, picked2 = []) => [...this.equipment.values()].filter(
|
|
3894
|
-
(m) =>
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3962
|
+
(m) => (
|
|
3963
|
+
/* 사람과 **같은 판정**(§capabilityOfResource) — 예전에는 이 자리에 조건을 늘어놓아서
|
|
3964
|
+
설비의 검사 결과는 배정에서 아예 읽히지 않았다(사람만 자격을 봤다). */
|
|
3965
|
+
this.capabilityOfResource(m, m.kind ? [m.kind] : [], this.classDefs.equipment).available && !picked2.includes(m.id) && /* 설비의 소속은 아직 단수(`kind`)다 — 저널에 기록이 쌓여 복수화를 별도 작업으로 두었다.
|
|
3966
|
+
다만 등급 정의가 있으면 **상속은 지금도 탄다**: 'welder-6axis' 가 'welder' 를 상속하면
|
|
3967
|
+
'welder' 요구를 만족한다. 단수/복수와 상속은 다른 축이므로 하나를 기다리지 않는다. */
|
|
3968
|
+
(kind === void 0 || classClosure(m.kind ? [m.kind] : [], this.classDefs.equipment, this.now()).has(kind))
|
|
3969
|
+
)
|
|
3899
3970
|
);
|
|
3900
3971
|
const need = this.operationSpecs.get(t.kind)?.equipmentSpecification;
|
|
3901
3972
|
if (!need?.length) {
|
|
@@ -5159,6 +5230,7 @@ function retiredVocabularyIn(line) {
|
|
|
5159
5230
|
axisInfo,
|
|
5160
5231
|
axisSource,
|
|
5161
5232
|
capabilitiesForType,
|
|
5233
|
+
capabilityOf,
|
|
5162
5234
|
classClosure,
|
|
5163
5235
|
compareStates,
|
|
5164
5236
|
computeOee,
|
|
@@ -5204,6 +5276,7 @@ function retiredVocabularyIn(line) {
|
|
|
5204
5276
|
relationsTo,
|
|
5205
5277
|
replay,
|
|
5206
5278
|
replaySegments,
|
|
5279
|
+
requiredTestsFor,
|
|
5207
5280
|
retiredVocabularyIn,
|
|
5208
5281
|
sgtinClass,
|
|
5209
5282
|
sgtinUri,
|
package/package.json
CHANGED