@operato/twin-kernel 0.6.5 → 0.6.7
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 +133 -0
- package/dist/contract.js +69 -0
- package/dist/flow-engine.d.ts +23 -1
- package/dist/flow-engine.js +44 -9
- package/dist-cjs/index.cjs +102 -6
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -168,6 +168,93 @@ export type TestSpecificationRefs = string[];
|
|
|
168
168
|
* 필요해지면 일정·실적과 같은 방식(`source: 'state'|'journal'`)으로 따로 낸다. 한 축에 둘을 담으면
|
|
169
169
|
* 선언과 관측을 가르는 이 트윈의 뼈대가 무너진다.
|
|
170
170
|
*/
|
|
171
|
+
/**
|
|
172
|
+
* 시험 결과 — **"이 개체가 그 시험을 통과했다"** 는 기록.
|
|
173
|
+
*
|
|
174
|
+
* ── 이것은 관측인데 왜 선언 경로로 오나 ───────────────────────────────────────
|
|
175
|
+
* 결과는 사실이 일어난 것이므로 관측이다. 그런데 그 사실을 **낳는 곳이 트윈이 아니다** — 자격 시험은
|
|
176
|
+
* 인사 시스템이, 설비 검사는 정비 시스템이 기록한다. 트윈은 그 기록을 **원본에서 받아** 안다.
|
|
177
|
+
* 그래서 이 값은 자원에 실려 인제스트로 들어온다(저널이 낳는 관측과 다른 길이다).
|
|
178
|
+
*
|
|
179
|
+
* ── 없으면 판정하지 않는다 ────────────────────────────────────────────────────
|
|
180
|
+
* 등급이 시험을 요구하는데(`ResourceClassDef.testSpecificationIds`) 그 사람에게 **결과가 아예 없으면
|
|
181
|
+
* 막지 않는다.** 없는 것으로 막으면 자격자가 전부 사라져 라인이 영구히 굶고, 그건 "선언한 것만 제약이
|
|
182
|
+
* 된다" 는 이 커널의 규율에도 어긋난다 — 원본이 결과를 주지 않는 현장에서는 그 제약이 선언되지 않은
|
|
183
|
+
* 것이다.
|
|
184
|
+
*
|
|
185
|
+
* **결과가 선언돼 있으면 그때부터 제약이 된다**: 불합격이거나 유효기간이 지났으면 그 등급으로 자격이
|
|
186
|
+
* 성립하지 않는다. 그것이 이 값을 싣는 이유다(싣지 않으면 아무 제약도 없다).
|
|
187
|
+
*
|
|
188
|
+
* 필드 이름은 최소로 둔다 — 표준 결과 타입(`B2MML-OperationsTest.xsd`)과 아직 대조하지 않았다.
|
|
189
|
+
*/
|
|
190
|
+
export interface TestResult {
|
|
191
|
+
/** 어느 시험인가 — `TestSpecification.id` 를 가리킨다. */
|
|
192
|
+
specId: string;
|
|
193
|
+
/** 합격 여부. 표준의 판정 어휘를 대조하기 전이므로 둘만 둔다(모르는 값을 만들지 않는다). */
|
|
194
|
+
result: 'pass' | 'fail';
|
|
195
|
+
/** 언제 통과·불합격했나(ISO). */
|
|
196
|
+
at?: ISOTime;
|
|
197
|
+
/**
|
|
198
|
+
* 언제까지 유효한가(ISO) — 자격에는 대개 유효기간이 있다.
|
|
199
|
+
*
|
|
200
|
+
* **없으면 무기한으로 본다.** 여기서 짐작으로 기한을 만들면 멀쩡한 자격자가 조용히 사라진다
|
|
201
|
+
* (유효기간을 모르는 것과 지난 것은 다르다).
|
|
202
|
+
*/
|
|
203
|
+
expiresAt?: ISOTime;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* 이 시험 결과가 **이 시각에 유효한 합격인가.**
|
|
207
|
+
*
|
|
208
|
+
* 판정을 한 곳에 둔다 — 자리마다 `result === 'pass'` 와 날짜 비교를 다시 적으면 한 곳이 빠지고,
|
|
209
|
+
* 빠진 쪽은 만료된 자격을 통과시킨다(조용한 결함).
|
|
210
|
+
*/
|
|
211
|
+
export declare function testPassedAt(r: TestResult, at?: ISOTime): boolean;
|
|
212
|
+
/**
|
|
213
|
+
* 이 개체가 **요구된 시험들을 만족하나** — 등급이 요구하고 개체가 기록을 든다.
|
|
214
|
+
*
|
|
215
|
+
* `required` 가 비면 요구가 없으므로 참이다. 요구된 시험에 **결과가 없으면 참**이다(위 머리말의 규율:
|
|
216
|
+
* 없는 것으로 막지 않는다). 결과가 있으면 그것이 유효한 합격이어야 한다.
|
|
217
|
+
*/
|
|
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
|
+
}
|
|
171
258
|
export interface TestSpecification {
|
|
172
259
|
/** 표준 `ID` — 자원의 `testSpecificationIds` 가 이 값을 가리킨다. */
|
|
173
260
|
id: string;
|
|
@@ -348,6 +435,13 @@ export interface MaterialDefinition extends EffectivePeriod {
|
|
|
348
435
|
properties?: ResourceProperty[];
|
|
349
436
|
/** 적격을 검증한 시험 명세들 — 표준 `MaterialDefinition.TestSpecificationID`. */
|
|
350
437
|
testSpecificationIds?: TestSpecificationRefs;
|
|
438
|
+
/**
|
|
439
|
+
* 그 시험들의 **결과** — "언제 통과했고 언제까지 유효한가"(§TestResult).
|
|
440
|
+
*
|
|
441
|
+
* 참조만 있으면 "무엇으로 검증한다" 까지고, 결과가 있어야 **자격이 성립하는지**를 말할 수 있다.
|
|
442
|
+
* 없으면 판정하지 않는다(없는 것으로 막으면 자격자가 전부 사라진다).
|
|
443
|
+
*/
|
|
444
|
+
testResults?: TestResult[];
|
|
351
445
|
}
|
|
352
446
|
/** 품목 정의 색인 — id(GTIN) → 정의. 소비처가 매번 배열을 훑지 않게. */
|
|
353
447
|
export type MaterialDefinitionIndex = ReadonlyMap<string, MaterialDefinition>;
|
|
@@ -523,6 +617,28 @@ export declare function offCalendarAt(r: {
|
|
|
523
617
|
};
|
|
524
618
|
workCalendar?: WorkCalendarEntry[];
|
|
525
619
|
}, ms: number, utcOffsetMinutes?: number): boolean;
|
|
620
|
+
/**
|
|
621
|
+
* 자원의 **가용 능력을 판정한다** — 하나의 규칙, 하나의 자리.
|
|
622
|
+
*
|
|
623
|
+
* 부르는 쪽이 시각과 시간대를 준다(커널은 `now()`·`utcOffsetMinutes`, 호스트는 관측 시각). 주지 않으면
|
|
624
|
+
* 시각에 달린 판정(유효기간·교대·시험 만료)은 **하지 않는다** — 모르면 판단하지 않는다는 규율이다.
|
|
625
|
+
*
|
|
626
|
+
* `requiredTests` 는 부르는 쪽이 등급에서 모아 넘긴다(등급 정의를 아는 것은 부르는 쪽이다).
|
|
627
|
+
*/
|
|
628
|
+
export declare function capabilityOf(r: {
|
|
629
|
+
status?: string;
|
|
630
|
+
held?: boolean;
|
|
631
|
+
window?: {
|
|
632
|
+
startHour: number;
|
|
633
|
+
endHour: number;
|
|
634
|
+
};
|
|
635
|
+
workCalendar?: WorkCalendarEntry[];
|
|
636
|
+
testResults?: TestResult[];
|
|
637
|
+
} & EffectivePeriod, ctx?: {
|
|
638
|
+
at?: ISOTime;
|
|
639
|
+
utcOffsetMinutes?: number;
|
|
640
|
+
requiredTests?: readonly string[];
|
|
641
|
+
}): Capability;
|
|
526
642
|
export interface LocationState {
|
|
527
643
|
id: string;
|
|
528
644
|
type: string;
|
|
@@ -734,6 +850,13 @@ export interface EquipmentState extends EffectivePeriod {
|
|
|
734
850
|
workCalendar?: WorkCalendarEntry[];
|
|
735
851
|
/** 적격을 검증한 시험 명세들 — 표준 `Equipment.TestSpecificationID`(§TestSpecificationRefs). */
|
|
736
852
|
testSpecificationIds?: TestSpecificationRefs;
|
|
853
|
+
/**
|
|
854
|
+
* 그 시험들의 **결과** — "언제 통과했고 언제까지 유효한가"(§TestResult).
|
|
855
|
+
*
|
|
856
|
+
* 참조만 있으면 "무엇으로 검증한다" 까지고, 결과가 있어야 **자격이 성립하는지**를 말할 수 있다.
|
|
857
|
+
* 없으면 판정하지 않는다(없는 것으로 막으면 자격자가 전부 사라진다).
|
|
858
|
+
*/
|
|
859
|
+
testResults?: TestResult[];
|
|
737
860
|
}
|
|
738
861
|
/**
|
|
739
862
|
* 사람 — **ISA-95 `Person`.** 설비와 다른 자원 종류다.
|
|
@@ -792,6 +915,13 @@ export interface PersonState extends EffectivePeriod {
|
|
|
792
915
|
workCalendar?: WorkCalendarEntry[];
|
|
793
916
|
/** 자격을 검증한 시험 명세들 — 표준 `Person.TestSpecificationID`(§TestSpecificationRefs). */
|
|
794
917
|
testSpecificationIds?: TestSpecificationRefs;
|
|
918
|
+
/**
|
|
919
|
+
* 그 시험들의 **결과** — "언제 통과했고 언제까지 유효한가"(§TestResult).
|
|
920
|
+
*
|
|
921
|
+
* 참조만 있으면 "무엇으로 검증한다" 까지고, 결과가 있어야 **자격이 성립하는지**를 말할 수 있다.
|
|
922
|
+
* 없으면 판정하지 않는다(없는 것으로 막으면 자격자가 전부 사라진다).
|
|
923
|
+
*/
|
|
924
|
+
testResults?: TestResult[];
|
|
795
925
|
}
|
|
796
926
|
/**
|
|
797
927
|
* 물리 자산 — **ISA-95 `PhysicalAsset`, GS1 `GRAI`(반복사용 자산).**
|
|
@@ -1385,6 +1515,7 @@ export interface TwinModelDef {
|
|
|
1385
1515
|
workCalendar?: WorkCalendarEntry[];
|
|
1386
1516
|
properties?: ResourceProperty[];
|
|
1387
1517
|
testSpecificationIds?: TestSpecificationRefs;
|
|
1518
|
+
testResults?: TestResult[];
|
|
1388
1519
|
})[];
|
|
1389
1520
|
/**
|
|
1390
1521
|
* 사람 — ISA-95 `Person`. `personnelClasses` 로 **속한 등급들**을 밝히고(복수가 표준), `window` 로
|
|
@@ -1402,6 +1533,7 @@ export interface TwinModelDef {
|
|
|
1402
1533
|
homeLocation?: string;
|
|
1403
1534
|
properties?: ResourceProperty[];
|
|
1404
1535
|
testSpecificationIds?: TestSpecificationRefs;
|
|
1536
|
+
testResults?: TestResult[];
|
|
1405
1537
|
})[];
|
|
1406
1538
|
/** 물리 자산(반복사용) — ISA-95 `PhysicalAsset` / GS1 `GRAI`. 선언하지 않으면 자산 제약이 없다. */
|
|
1407
1539
|
assets?: (EffectivePeriod & {
|
|
@@ -1410,6 +1542,7 @@ export interface TwinModelDef {
|
|
|
1410
1542
|
homeLocation?: string;
|
|
1411
1543
|
properties?: ResourceProperty[];
|
|
1412
1544
|
testSpecificationIds?: TestSpecificationRefs;
|
|
1545
|
+
testResults?: TestResult[];
|
|
1413
1546
|
})[];
|
|
1414
1547
|
/**
|
|
1415
1548
|
* 이 트윈의 **시각 해석 기준**(UTC 로부터의 분). 근무 캘린더의 `HH:MM` 이 어느 기준인지 정한다.
|
package/dist/contract.js
CHANGED
|
@@ -127,6 +127,38 @@ export function hierarchyOf(s) {
|
|
|
127
127
|
}
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* 이 시험 결과가 **이 시각에 유효한 합격인가.**
|
|
132
|
+
*
|
|
133
|
+
* 판정을 한 곳에 둔다 — 자리마다 `result === 'pass'` 와 날짜 비교를 다시 적으면 한 곳이 빠지고,
|
|
134
|
+
* 빠진 쪽은 만료된 자격을 통과시킨다(조용한 결함).
|
|
135
|
+
*/
|
|
136
|
+
export function testPassedAt(r, at) {
|
|
137
|
+
if (r.result !== 'pass')
|
|
138
|
+
return false;
|
|
139
|
+
if (!r.expiresAt || !at)
|
|
140
|
+
return r.result === 'pass';
|
|
141
|
+
return Date.parse(at) <= Date.parse(r.expiresAt);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* 이 개체가 **요구된 시험들을 만족하나** — 등급이 요구하고 개체가 기록을 든다.
|
|
145
|
+
*
|
|
146
|
+
* `required` 가 비면 요구가 없으므로 참이다. 요구된 시험에 **결과가 없으면 참**이다(위 머리말의 규율:
|
|
147
|
+
* 없는 것으로 막지 않는다). 결과가 있으면 그것이 유효한 합격이어야 한다.
|
|
148
|
+
*/
|
|
149
|
+
export function meetsTests(required, results, at) {
|
|
150
|
+
if (!required.length)
|
|
151
|
+
return true;
|
|
152
|
+
const bySpec = new Map((results ?? []).map(r => [r.specId, r]));
|
|
153
|
+
for (const specId of required) {
|
|
154
|
+
const r = bySpec.get(specId);
|
|
155
|
+
if (!r)
|
|
156
|
+
continue; // 결과가 선언되지 않았다 — 제약이 아니다
|
|
157
|
+
if (!testPassedAt(r, at))
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
130
162
|
/**
|
|
131
163
|
* 이 시각에 유효 기간 밖인가 — **한 규칙**으로 개체·등급·설비↔자산 매핑을 모두 판정한다.
|
|
132
164
|
*
|
|
@@ -485,6 +517,43 @@ export function offCalendarAt(r, ms, utcOffsetMinutes) {
|
|
|
485
517
|
const h = Math.floor(minute / 60);
|
|
486
518
|
return !(w.startHour <= w.endHour ? h >= w.startHour && h < w.endHour : h >= w.startHour || h < w.endHour);
|
|
487
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* 자원의 **가용 능력을 판정한다** — 하나의 규칙, 하나의 자리.
|
|
522
|
+
*
|
|
523
|
+
* 부르는 쪽이 시각과 시간대를 준다(커널은 `now()`·`utcOffsetMinutes`, 호스트는 관측 시각). 주지 않으면
|
|
524
|
+
* 시각에 달린 판정(유효기간·교대·시험 만료)은 **하지 않는다** — 모르면 판단하지 않는다는 규율이다.
|
|
525
|
+
*
|
|
526
|
+
* `requiredTests` 는 부르는 쪽이 등급에서 모아 넘긴다(등급 정의를 아는 것은 부르는 쪽이다).
|
|
527
|
+
*/
|
|
528
|
+
export function capabilityOf(r, ctx) {
|
|
529
|
+
const at = ctx?.at;
|
|
530
|
+
/* 순서가 뜻이다 — "이미 모델 밖" 이 "고장" 보다 앞선다(폐기한 설비의 고장은 고칠 일이 아니다). */
|
|
531
|
+
const eff = effectivityAt(r, at);
|
|
532
|
+
if (eff === 'not-yet')
|
|
533
|
+
return { available: false, reason: 'not-yet' };
|
|
534
|
+
if (eff === 'expired')
|
|
535
|
+
return { available: false, reason: 'retired' };
|
|
536
|
+
if (r.held)
|
|
537
|
+
return { available: false, reason: 'held' };
|
|
538
|
+
if (r.status === 'down')
|
|
539
|
+
return { available: false, reason: 'down' };
|
|
540
|
+
if (at) {
|
|
541
|
+
const ms = Date.parse(at);
|
|
542
|
+
if (Number.isFinite(ms)) {
|
|
543
|
+
const why = offCalendarReasonAt(r, ms, ctx?.utcOffsetMinutes);
|
|
544
|
+
if (why === 'non-working')
|
|
545
|
+
return { available: false, reason: 'resting' };
|
|
546
|
+
if (why === 'off-hours')
|
|
547
|
+
return { available: false, reason: 'off-shift' };
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
/* 자격은 **결과가 선언됐을 때만** 제약이다(§meetsTests) — 없는 것으로 막으면 라인이 굶는다. */
|
|
551
|
+
if (ctx?.requiredTests?.length && !meetsTests(ctx.requiredTests, r.testResults, at))
|
|
552
|
+
return { available: false, reason: 'test-expired' };
|
|
553
|
+
if (r.status && r.status !== 'idle' && r.status !== 'available')
|
|
554
|
+
return { available: false, reason: 'working' };
|
|
555
|
+
return { available: true, reason: 'available' };
|
|
556
|
+
}
|
|
488
557
|
// ── 운영 델타(비-EPCIS) — State 채널의 나머지 절반 ──────────────────────────
|
|
489
558
|
// EPCIS 이벤트는 재고/위치만 재구성 가능. tasks·equipment·orders 의 운영 상태는
|
|
490
559
|
// 이 델타로 미러한다. envelope.eventType = 'task.status' | 'equipment.status' | 'order.status'.
|
package/dist/flow-engine.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ISOTime, MaterialQuantity, WorkCalendarEntry, EffectivePeriod, Effectivity, OffCalendarReason, ResourceProperty, ResourceClassDef, MaterialDefinition, Attention, TwinModelDef, CanonicalEnvelope, Command, CommandAck, EventHandler, EquipmentMotion, OeeMetrics, AssetState, GeneratorSpec, OrderState, PersonState, ScenarioControl, StateSnapshot, TwinKernel, Unsubscribe, LocationState, ItemState, EquipmentState, OrderStatusDelta, TaskState, StructureShift } from './contract.ts';
|
|
1
|
+
import type { TestResult, ISOTime, MaterialQuantity, WorkCalendarEntry, EffectivePeriod, Effectivity, OffCalendarReason, ResourceProperty, ResourceClassDef, MaterialDefinition, Attention, TwinModelDef, CanonicalEnvelope, Command, CommandAck, EventHandler, EquipmentMotion, OeeMetrics, AssetState, GeneratorSpec, OrderState, PersonState, ScenarioControl, StateSnapshot, TwinKernel, Unsubscribe, LocationState, ItemState, EquipmentState, OrderStatusDelta, TaskState, StructureShift } from './contract.ts';
|
|
2
2
|
import type { EpcisEvent, BizTransactionElement } from './epcis.ts';
|
|
3
3
|
import type { AllocationPolicy, SlotView } from './allocation-policy.ts';
|
|
4
4
|
import type { DurationEstimator, DurationContext } from './duration-estimator.ts';
|
|
@@ -76,6 +76,13 @@ export interface FlowPerson extends EffectivePeriod {
|
|
|
76
76
|
properties?: ResourceProperty[];
|
|
77
77
|
/** 자격을 검증한 시험 명세들 — 표준 `TestSpecificationID`. */
|
|
78
78
|
testSpecificationIds?: string[];
|
|
79
|
+
/**
|
|
80
|
+
* 그 시험들의 **결과** — 자격이 성립하는지는 이것이 말한다(§TestResult).
|
|
81
|
+
*
|
|
82
|
+
* 참조만 있으면 "무엇으로 검증한다" 까지고, 배정 판정은 결과가 있어야 할 수 있다. 없으면 판정하지
|
|
83
|
+
* 않는다(없는 것으로 막으면 자격자가 전부 사라진다).
|
|
84
|
+
*/
|
|
85
|
+
testResults?: TestResult[];
|
|
79
86
|
}
|
|
80
87
|
export interface FlowEquipment extends EffectivePeriod {
|
|
81
88
|
id: string;
|
|
@@ -793,6 +800,21 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
793
800
|
private recordMaterialActual;
|
|
794
801
|
/** 확보한 자재를 **작업 시작 시점에 소비**한다 — 수량이 0 이 되면 물품 자체가 사라진다. */
|
|
795
802
|
private consumeMaterials;
|
|
803
|
+
/**
|
|
804
|
+
* 이 사람이 **속한 등급들이 요구하는 시험**을 만족하나.
|
|
805
|
+
*
|
|
806
|
+
* 요구는 등급이 말하고(`ResourceClassDef.testSpecificationIds`) 기록은 사람이 든다. 상속을 타고 닫은
|
|
807
|
+
* 등급 전부의 요구를 모아 본다 — 상위 등급이 요구하는 시험도 자격의 조건이다.
|
|
808
|
+
*
|
|
809
|
+
* 판정 규칙은 계약이 소유한다(`meetsTests`) — 여기서 다시 적으면 규칙이 두 곳이 되고, 한쪽이 만료를
|
|
810
|
+
* 조용히 통과시킨다.
|
|
811
|
+
*/
|
|
812
|
+
/**
|
|
813
|
+
* 이 사람이 속한 등급들이 **요구하는 시험 목록** — 판정은 계약이 한다(`capabilityOf`).
|
|
814
|
+
*
|
|
815
|
+
* 상속을 타고 닫은 등급 전부의 요구를 모은다: 상위 등급이 요구하는 시험도 자격의 조건이다.
|
|
816
|
+
*/
|
|
817
|
+
private requiredTestsOf;
|
|
796
818
|
private claimPersonnel;
|
|
797
819
|
/**
|
|
798
820
|
* 필요 설비를 고른다 — **인원·자산과 같은 규칙**(등급으로 요구, 부분 확보 없이 전량 아니면 대기).
|
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, priorityRank, dueStatusOf, effectivityAt, offCalendarAt, offCalendarReasonAt, minuteOfDayAt, activeShiftAt, subLotIdOf, itemKeyOf } from "./contract.js";
|
|
12
|
+
import { OP_EVENT, CMD, locationStatusOf, readBoardEquipment, readBoardLocations, readBoardAssets, classClosure, capabilityOf, 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";
|
|
@@ -284,10 +284,12 @@ export class FlowEngine {
|
|
|
284
284
|
return { id: n.id, type: n.type, capacity: n.capacity, parallelism: n.parallelism, occupancy: 0, status: 'idle', parentId: n.parentId };
|
|
285
285
|
}
|
|
286
286
|
buildPerson(p) {
|
|
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 } : {}),
|
|
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.testResults ? { testResults: p.testResults } : {}), ...(p.testSpecificationIds ? { testSpecificationIds: p.testSpecificationIds } : {}), ...(p.testResults ? { testResults: p.testResults } : {}), ...effectiveOnly(p) };
|
|
288
289
|
}
|
|
289
290
|
buildAsset(a) {
|
|
290
|
-
return { id: a.id, assetClassIds: a.assetClassIds, location: a.homeLocation, status: 'idle', taskId: null, ...(a.properties ? { properties: a.properties } : {}),
|
|
291
|
+
return { id: a.id, assetClassIds: a.assetClassIds, location: a.homeLocation, status: 'idle', taskId: null, ...(a.properties ? { properties: a.properties } : {}),
|
|
292
|
+
...(a.testResults ? { testResults: a.testResults } : {}), ...(a.testSpecificationIds ? { testSpecificationIds: a.testSpecificationIds } : {}), ...(a.testResults ? { testResults: a.testResults } : {}), ...effectiveOnly(a) };
|
|
291
293
|
}
|
|
292
294
|
/**
|
|
293
295
|
* 이 커널이 **관측으로 구동된다**고 선언한다 — 미러가 첫 이벤트를 받기 전에도 그렇다.
|
|
@@ -1843,6 +1845,31 @@ export class FlowEngine {
|
|
|
1843
1845
|
});
|
|
1844
1846
|
}
|
|
1845
1847
|
}
|
|
1848
|
+
/**
|
|
1849
|
+
* 이 사람이 **속한 등급들이 요구하는 시험**을 만족하나.
|
|
1850
|
+
*
|
|
1851
|
+
* 요구는 등급이 말하고(`ResourceClassDef.testSpecificationIds`) 기록은 사람이 든다. 상속을 타고 닫은
|
|
1852
|
+
* 등급 전부의 요구를 모아 본다 — 상위 등급이 요구하는 시험도 자격의 조건이다.
|
|
1853
|
+
*
|
|
1854
|
+
* 판정 규칙은 계약이 소유한다(`meetsTests`) — 여기서 다시 적으면 규칙이 두 곳이 되고, 한쪽이 만료를
|
|
1855
|
+
* 조용히 통과시킨다.
|
|
1856
|
+
*/
|
|
1857
|
+
/**
|
|
1858
|
+
* 이 사람이 속한 등급들이 **요구하는 시험 목록** — 판정은 계약이 한다(`capabilityOf`).
|
|
1859
|
+
*
|
|
1860
|
+
* 상속을 타고 닫은 등급 전부의 요구를 모은다: 상위 등급이 요구하는 시험도 자격의 조건이다.
|
|
1861
|
+
*/
|
|
1862
|
+
requiredTestsOf(p) {
|
|
1863
|
+
const defs = this.classDefs.personnel;
|
|
1864
|
+
if (!defs?.length)
|
|
1865
|
+
return [];
|
|
1866
|
+
const closure = classClosure(p.personnelClassIds, defs, this.now());
|
|
1867
|
+
const required = [];
|
|
1868
|
+
for (const d of defs)
|
|
1869
|
+
if (closure.has(d.id))
|
|
1870
|
+
required.push(...(d.testSpecificationIds ?? []));
|
|
1871
|
+
return required;
|
|
1872
|
+
}
|
|
1846
1873
|
claimPersonnel(t) {
|
|
1847
1874
|
const need = this.operationSpecs.get(t.kind)?.personnelSpecification;
|
|
1848
1875
|
if (!need?.length)
|
|
@@ -1852,13 +1879,21 @@ export class FlowEngine {
|
|
|
1852
1879
|
const want = Math.max(0, Math.floor(req.quantity ?? 0));
|
|
1853
1880
|
if (!want)
|
|
1854
1881
|
continue;
|
|
1855
|
-
const avail = [...this.persons.values()].filter(p => p.
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1882
|
+
const avail = [...this.persons.values()].filter(p => !picked.includes(p.id) &&
|
|
1883
|
+
/*
|
|
1884
|
+
* **판정은 한 벌이다**(`capabilityOf`). 예전에는 이 자리에 조건을 늘어놓았고 화면은 같은
|
|
1885
|
+
* 판정을 자기 코드로 다시 만들었다 — 규칙이 두 벌이면 갈라지고, 배정은 막는데 화면은
|
|
1886
|
+
* "가용" 이라 말하는 순간이 온다(그 어긋남은 조용하다).
|
|
1887
|
+
*
|
|
1888
|
+
* 요구된 시험은 등급에서 모아 넘긴다 — 등급 정의를 아는 것은 이쪽이다.
|
|
1889
|
+
*/
|
|
1890
|
+
capabilityOf(p, {
|
|
1891
|
+
at: this.now(),
|
|
1892
|
+
utcOffsetMinutes: this.boardDef?.utcOffsetMinutes,
|
|
1893
|
+
requiredTests: this.requiredTestsOf(p)
|
|
1894
|
+
}).available &&
|
|
1859
1895
|
/* 자격은 **상속을 타고 닫아** 판정한다 — 사람은 여러 등급에 속할 수 있고(표준), 등급은
|
|
1860
|
-
다른 등급을 상속한다. "생산직 2명" 요구를 "용접 자격자" 가 만족해야 한다.
|
|
1861
|
-
유효기간 밖의 등급은 닫힘에서 빠진다(만료된 자격으로 배정되지 않는다). */
|
|
1896
|
+
다른 등급을 상속한다. "생산직 2명" 요구를 "용접 자격자" 가 만족해야 한다. */
|
|
1862
1897
|
(req.personnelClass === undefined ||
|
|
1863
1898
|
classClosure(p.personnelClassIds, this.classDefs.personnel, this.now()).has(req.personnelClass)));
|
|
1864
1899
|
if (avail.length < want)
|
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,
|
|
@@ -99,6 +100,7 @@ __export(index_exports, {
|
|
|
99
100
|
lgtinClass: () => lgtinClass,
|
|
100
101
|
locationStatusOf: () => locationStatusOf,
|
|
101
102
|
mapRecord: () => mapRecord,
|
|
103
|
+
meetsTests: () => meetsTests,
|
|
102
104
|
minuteOfDayAt: () => minuteOfDayAt,
|
|
103
105
|
monteCarloForecast: () => monteCarloForecast,
|
|
104
106
|
objectEvent: () => objectEvent,
|
|
@@ -123,6 +125,7 @@ __export(index_exports, {
|
|
|
123
125
|
ssccUri: () => ssccUri,
|
|
124
126
|
stateFieldsOf: () => stateFieldsOf,
|
|
125
127
|
subLotIdOf: () => subLotIdOf,
|
|
128
|
+
testPassedAt: () => testPassedAt,
|
|
126
129
|
transactionEvent: () => transactionEvent,
|
|
127
130
|
transformationEvent: () => transformationEvent,
|
|
128
131
|
validateDomainDefinition: () => validateDomainDefinition,
|
|
@@ -218,6 +221,21 @@ function hierarchyOf(s) {
|
|
|
218
221
|
}
|
|
219
222
|
};
|
|
220
223
|
}
|
|
224
|
+
function testPassedAt(r, at) {
|
|
225
|
+
if (r.result !== "pass") return false;
|
|
226
|
+
if (!r.expiresAt || !at) return r.result === "pass";
|
|
227
|
+
return Date.parse(at) <= Date.parse(r.expiresAt);
|
|
228
|
+
}
|
|
229
|
+
function meetsTests(required, results, at) {
|
|
230
|
+
if (!required.length) return true;
|
|
231
|
+
const bySpec = new Map((results ?? []).map((r) => [r.specId, r]));
|
|
232
|
+
for (const specId of required) {
|
|
233
|
+
const r = bySpec.get(specId);
|
|
234
|
+
if (!r) continue;
|
|
235
|
+
if (!testPassedAt(r, at)) return false;
|
|
236
|
+
}
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
221
239
|
function effectivityAt(p, at) {
|
|
222
240
|
if (!p || !at) return void 0;
|
|
223
241
|
const atMs = Date.parse(at);
|
|
@@ -392,6 +410,26 @@ function offCalendarAt(r, ms2, utcOffsetMinutes) {
|
|
|
392
410
|
const h = Math.floor(minute / 60);
|
|
393
411
|
return !(w.startHour <= w.endHour ? h >= w.startHour && h < w.endHour : h >= w.startHour || h < w.endHour);
|
|
394
412
|
}
|
|
413
|
+
function capabilityOf(r, ctx) {
|
|
414
|
+
const at = ctx?.at;
|
|
415
|
+
const eff = effectivityAt(r, at);
|
|
416
|
+
if (eff === "not-yet") return { available: false, reason: "not-yet" };
|
|
417
|
+
if (eff === "expired") return { available: false, reason: "retired" };
|
|
418
|
+
if (r.held) return { available: false, reason: "held" };
|
|
419
|
+
if (r.status === "down") return { available: false, reason: "down" };
|
|
420
|
+
if (at) {
|
|
421
|
+
const ms2 = Date.parse(at);
|
|
422
|
+
if (Number.isFinite(ms2)) {
|
|
423
|
+
const why = offCalendarReasonAt(r, ms2, ctx?.utcOffsetMinutes);
|
|
424
|
+
if (why === "non-working") return { available: false, reason: "resting" };
|
|
425
|
+
if (why === "off-hours") return { available: false, reason: "off-shift" };
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (ctx?.requiredTests?.length && !meetsTests(ctx.requiredTests, r.testResults, at))
|
|
429
|
+
return { available: false, reason: "test-expired" };
|
|
430
|
+
if (r.status && r.status !== "idle" && r.status !== "available") return { available: false, reason: "working" };
|
|
431
|
+
return { available: true, reason: "available" };
|
|
432
|
+
}
|
|
395
433
|
var OP_EVENT = {
|
|
396
434
|
task: "task.status",
|
|
397
435
|
equipment: "equipment.status",
|
|
@@ -2394,10 +2432,34 @@ var FlowEngine = class {
|
|
|
2394
2432
|
return { id: n.id, type: n.type, capacity: n.capacity, parallelism: n.parallelism, occupancy: 0, status: "idle", parentId: n.parentId };
|
|
2395
2433
|
}
|
|
2396
2434
|
buildPerson(p) {
|
|
2397
|
-
return {
|
|
2435
|
+
return {
|
|
2436
|
+
id: p.id,
|
|
2437
|
+
personnelClassIds: p.personnelClassIds,
|
|
2438
|
+
status: "idle",
|
|
2439
|
+
taskId: null,
|
|
2440
|
+
window: p.window,
|
|
2441
|
+
...p.workCalendar ? { workCalendar: p.workCalendar } : {},
|
|
2442
|
+
...p.homeLocation ? { location: p.homeLocation } : {},
|
|
2443
|
+
...p.properties ? { properties: p.properties } : {},
|
|
2444
|
+
...p.testResults ? { testResults: p.testResults } : {},
|
|
2445
|
+
...p.testSpecificationIds ? { testSpecificationIds: p.testSpecificationIds } : {},
|
|
2446
|
+
...p.testResults ? { testResults: p.testResults } : {},
|
|
2447
|
+
...effectiveOnly(p)
|
|
2448
|
+
};
|
|
2398
2449
|
}
|
|
2399
2450
|
buildAsset(a) {
|
|
2400
|
-
return {
|
|
2451
|
+
return {
|
|
2452
|
+
id: a.id,
|
|
2453
|
+
assetClassIds: a.assetClassIds,
|
|
2454
|
+
location: a.homeLocation,
|
|
2455
|
+
status: "idle",
|
|
2456
|
+
taskId: null,
|
|
2457
|
+
...a.properties ? { properties: a.properties } : {},
|
|
2458
|
+
...a.testResults ? { testResults: a.testResults } : {},
|
|
2459
|
+
...a.testSpecificationIds ? { testSpecificationIds: a.testSpecificationIds } : {},
|
|
2460
|
+
...a.testResults ? { testResults: a.testResults } : {},
|
|
2461
|
+
...effectiveOnly(a)
|
|
2462
|
+
};
|
|
2401
2463
|
}
|
|
2402
2464
|
/**
|
|
2403
2465
|
* 이 커널이 **관측으로 구동된다**고 선언한다 — 미러가 첫 이벤트를 받기 전에도 그렇다.
|
|
@@ -3799,6 +3861,28 @@ var FlowEngine = class {
|
|
|
3799
3861
|
});
|
|
3800
3862
|
}
|
|
3801
3863
|
}
|
|
3864
|
+
/**
|
|
3865
|
+
* 이 사람이 **속한 등급들이 요구하는 시험**을 만족하나.
|
|
3866
|
+
*
|
|
3867
|
+
* 요구는 등급이 말하고(`ResourceClassDef.testSpecificationIds`) 기록은 사람이 든다. 상속을 타고 닫은
|
|
3868
|
+
* 등급 전부의 요구를 모아 본다 — 상위 등급이 요구하는 시험도 자격의 조건이다.
|
|
3869
|
+
*
|
|
3870
|
+
* 판정 규칙은 계약이 소유한다(`meetsTests`) — 여기서 다시 적으면 규칙이 두 곳이 되고, 한쪽이 만료를
|
|
3871
|
+
* 조용히 통과시킨다.
|
|
3872
|
+
*/
|
|
3873
|
+
/**
|
|
3874
|
+
* 이 사람이 속한 등급들이 **요구하는 시험 목록** — 판정은 계약이 한다(`capabilityOf`).
|
|
3875
|
+
*
|
|
3876
|
+
* 상속을 타고 닫은 등급 전부의 요구를 모은다: 상위 등급이 요구하는 시험도 자격의 조건이다.
|
|
3877
|
+
*/
|
|
3878
|
+
requiredTestsOf(p) {
|
|
3879
|
+
const defs = this.classDefs.personnel;
|
|
3880
|
+
if (!defs?.length) return [];
|
|
3881
|
+
const closure = classClosure(p.personnelClassIds, defs, this.now());
|
|
3882
|
+
const required = [];
|
|
3883
|
+
for (const d of defs) if (closure.has(d.id)) required.push(...d.testSpecificationIds ?? []);
|
|
3884
|
+
return required;
|
|
3885
|
+
}
|
|
3802
3886
|
claimPersonnel(t) {
|
|
3803
3887
|
const need = this.operationSpecs.get(t.kind)?.personnelSpecification;
|
|
3804
3888
|
if (!need?.length) return [];
|
|
@@ -3807,10 +3891,19 @@ var FlowEngine = class {
|
|
|
3807
3891
|
const want = Math.max(0, Math.floor(req.quantity ?? 0));
|
|
3808
3892
|
if (!want) continue;
|
|
3809
3893
|
const avail = [...this.persons.values()].filter(
|
|
3810
|
-
(p) =>
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3894
|
+
(p) => !picked.includes(p.id) && /*
|
|
3895
|
+
* **판정은 한 벌이다**(`capabilityOf`). 예전에는 이 자리에 조건을 늘어놓았고 화면은 같은
|
|
3896
|
+
* 판정을 자기 코드로 다시 만들었다 — 규칙이 두 벌이면 갈라지고, 배정은 막는데 화면은
|
|
3897
|
+
* "가용" 이라 말하는 순간이 온다(그 어긋남은 조용하다).
|
|
3898
|
+
*
|
|
3899
|
+
* 요구된 시험은 등급에서 모아 넘긴다 — 등급 정의를 아는 것은 이쪽이다.
|
|
3900
|
+
*/
|
|
3901
|
+
capabilityOf(p, {
|
|
3902
|
+
at: this.now(),
|
|
3903
|
+
utcOffsetMinutes: this.boardDef?.utcOffsetMinutes,
|
|
3904
|
+
requiredTests: this.requiredTestsOf(p)
|
|
3905
|
+
}).available && /* 자격은 **상속을 타고 닫아** 판정한다 — 사람은 여러 등급에 속할 수 있고(표준), 등급은
|
|
3906
|
+
다른 등급을 상속한다. "생산직 2명" 요구를 "용접 자격자" 가 만족해야 한다. */
|
|
3814
3907
|
(req.personnelClass === void 0 || classClosure(p.personnelClassIds, this.classDefs.personnel, this.now()).has(req.personnelClass))
|
|
3815
3908
|
);
|
|
3816
3909
|
if (avail.length < want) return null;
|
|
@@ -5093,6 +5186,7 @@ function retiredVocabularyIn(line) {
|
|
|
5093
5186
|
axisInfo,
|
|
5094
5187
|
axisSource,
|
|
5095
5188
|
capabilitiesForType,
|
|
5189
|
+
capabilityOf,
|
|
5096
5190
|
classClosure,
|
|
5097
5191
|
compareStates,
|
|
5098
5192
|
computeOee,
|
|
@@ -5119,6 +5213,7 @@ function retiredVocabularyIn(line) {
|
|
|
5119
5213
|
lgtinClass,
|
|
5120
5214
|
locationStatusOf,
|
|
5121
5215
|
mapRecord,
|
|
5216
|
+
meetsTests,
|
|
5122
5217
|
minuteOfDayAt,
|
|
5123
5218
|
monteCarloForecast,
|
|
5124
5219
|
objectEvent,
|
|
@@ -5143,6 +5238,7 @@ function retiredVocabularyIn(line) {
|
|
|
5143
5238
|
ssccUri,
|
|
5144
5239
|
stateFieldsOf,
|
|
5145
5240
|
subLotIdOf,
|
|
5241
|
+
testPassedAt,
|
|
5146
5242
|
transactionEvent,
|
|
5147
5243
|
transformationEvent,
|
|
5148
5244
|
validateDomainDefinition,
|
package/package.json
CHANGED