@operato/twin-kernel 0.6.5 → 0.6.6
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 +72 -0
- package/dist/contract.js +32 -0
- package/dist/flow-engine.d.ts +18 -1
- package/dist/flow-engine.js +35 -4
- package/dist-cjs/index.cjs +71 -3
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -168,6 +168,54 @@ 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;
|
|
171
219
|
export interface TestSpecification {
|
|
172
220
|
/** 표준 `ID` — 자원의 `testSpecificationIds` 가 이 값을 가리킨다. */
|
|
173
221
|
id: string;
|
|
@@ -348,6 +396,13 @@ export interface MaterialDefinition extends EffectivePeriod {
|
|
|
348
396
|
properties?: ResourceProperty[];
|
|
349
397
|
/** 적격을 검증한 시험 명세들 — 표준 `MaterialDefinition.TestSpecificationID`. */
|
|
350
398
|
testSpecificationIds?: TestSpecificationRefs;
|
|
399
|
+
/**
|
|
400
|
+
* 그 시험들의 **결과** — "언제 통과했고 언제까지 유효한가"(§TestResult).
|
|
401
|
+
*
|
|
402
|
+
* 참조만 있으면 "무엇으로 검증한다" 까지고, 결과가 있어야 **자격이 성립하는지**를 말할 수 있다.
|
|
403
|
+
* 없으면 판정하지 않는다(없는 것으로 막으면 자격자가 전부 사라진다).
|
|
404
|
+
*/
|
|
405
|
+
testResults?: TestResult[];
|
|
351
406
|
}
|
|
352
407
|
/** 품목 정의 색인 — id(GTIN) → 정의. 소비처가 매번 배열을 훑지 않게. */
|
|
353
408
|
export type MaterialDefinitionIndex = ReadonlyMap<string, MaterialDefinition>;
|
|
@@ -734,6 +789,13 @@ export interface EquipmentState extends EffectivePeriod {
|
|
|
734
789
|
workCalendar?: WorkCalendarEntry[];
|
|
735
790
|
/** 적격을 검증한 시험 명세들 — 표준 `Equipment.TestSpecificationID`(§TestSpecificationRefs). */
|
|
736
791
|
testSpecificationIds?: TestSpecificationRefs;
|
|
792
|
+
/**
|
|
793
|
+
* 그 시험들의 **결과** — "언제 통과했고 언제까지 유효한가"(§TestResult).
|
|
794
|
+
*
|
|
795
|
+
* 참조만 있으면 "무엇으로 검증한다" 까지고, 결과가 있어야 **자격이 성립하는지**를 말할 수 있다.
|
|
796
|
+
* 없으면 판정하지 않는다(없는 것으로 막으면 자격자가 전부 사라진다).
|
|
797
|
+
*/
|
|
798
|
+
testResults?: TestResult[];
|
|
737
799
|
}
|
|
738
800
|
/**
|
|
739
801
|
* 사람 — **ISA-95 `Person`.** 설비와 다른 자원 종류다.
|
|
@@ -792,6 +854,13 @@ export interface PersonState extends EffectivePeriod {
|
|
|
792
854
|
workCalendar?: WorkCalendarEntry[];
|
|
793
855
|
/** 자격을 검증한 시험 명세들 — 표준 `Person.TestSpecificationID`(§TestSpecificationRefs). */
|
|
794
856
|
testSpecificationIds?: TestSpecificationRefs;
|
|
857
|
+
/**
|
|
858
|
+
* 그 시험들의 **결과** — "언제 통과했고 언제까지 유효한가"(§TestResult).
|
|
859
|
+
*
|
|
860
|
+
* 참조만 있으면 "무엇으로 검증한다" 까지고, 결과가 있어야 **자격이 성립하는지**를 말할 수 있다.
|
|
861
|
+
* 없으면 판정하지 않는다(없는 것으로 막으면 자격자가 전부 사라진다).
|
|
862
|
+
*/
|
|
863
|
+
testResults?: TestResult[];
|
|
795
864
|
}
|
|
796
865
|
/**
|
|
797
866
|
* 물리 자산 — **ISA-95 `PhysicalAsset`, GS1 `GRAI`(반복사용 자산).**
|
|
@@ -1385,6 +1454,7 @@ export interface TwinModelDef {
|
|
|
1385
1454
|
workCalendar?: WorkCalendarEntry[];
|
|
1386
1455
|
properties?: ResourceProperty[];
|
|
1387
1456
|
testSpecificationIds?: TestSpecificationRefs;
|
|
1457
|
+
testResults?: TestResult[];
|
|
1388
1458
|
})[];
|
|
1389
1459
|
/**
|
|
1390
1460
|
* 사람 — ISA-95 `Person`. `personnelClasses` 로 **속한 등급들**을 밝히고(복수가 표준), `window` 로
|
|
@@ -1402,6 +1472,7 @@ export interface TwinModelDef {
|
|
|
1402
1472
|
homeLocation?: string;
|
|
1403
1473
|
properties?: ResourceProperty[];
|
|
1404
1474
|
testSpecificationIds?: TestSpecificationRefs;
|
|
1475
|
+
testResults?: TestResult[];
|
|
1405
1476
|
})[];
|
|
1406
1477
|
/** 물리 자산(반복사용) — ISA-95 `PhysicalAsset` / GS1 `GRAI`. 선언하지 않으면 자산 제약이 없다. */
|
|
1407
1478
|
assets?: (EffectivePeriod & {
|
|
@@ -1410,6 +1481,7 @@ export interface TwinModelDef {
|
|
|
1410
1481
|
homeLocation?: string;
|
|
1411
1482
|
properties?: ResourceProperty[];
|
|
1412
1483
|
testSpecificationIds?: TestSpecificationRefs;
|
|
1484
|
+
testResults?: TestResult[];
|
|
1413
1485
|
})[];
|
|
1414
1486
|
/**
|
|
1415
1487
|
* 이 트윈의 **시각 해석 기준**(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
|
*
|
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,16 @@ 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
|
+
private qualifiedByTests;
|
|
796
813
|
private claimPersonnel;
|
|
797
814
|
/**
|
|
798
815
|
* 필요 설비를 고른다 — **인원·자산과 같은 규칙**(등급으로 요구, 부분 확보 없이 전량 아니면 대기).
|
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, meetsTests, 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,26 @@ export class FlowEngine {
|
|
|
1843
1845
|
});
|
|
1844
1846
|
}
|
|
1845
1847
|
}
|
|
1848
|
+
/**
|
|
1849
|
+
* 이 사람이 **속한 등급들이 요구하는 시험**을 만족하나.
|
|
1850
|
+
*
|
|
1851
|
+
* 요구는 등급이 말하고(`ResourceClassDef.testSpecificationIds`) 기록은 사람이 든다. 상속을 타고 닫은
|
|
1852
|
+
* 등급 전부의 요구를 모아 본다 — 상위 등급이 요구하는 시험도 자격의 조건이다.
|
|
1853
|
+
*
|
|
1854
|
+
* 판정 규칙은 계약이 소유한다(`meetsTests`) — 여기서 다시 적으면 규칙이 두 곳이 되고, 한쪽이 만료를
|
|
1855
|
+
* 조용히 통과시킨다.
|
|
1856
|
+
*/
|
|
1857
|
+
qualifiedByTests(p) {
|
|
1858
|
+
const defs = this.classDefs.personnel;
|
|
1859
|
+
if (!defs?.length)
|
|
1860
|
+
return true;
|
|
1861
|
+
const closure = classClosure(p.personnelClassIds, defs, this.now());
|
|
1862
|
+
const required = [];
|
|
1863
|
+
for (const d of defs)
|
|
1864
|
+
if (closure.has(d.id))
|
|
1865
|
+
required.push(...(d.testSpecificationIds ?? []));
|
|
1866
|
+
return meetsTests(required, p.testResults, this.now());
|
|
1867
|
+
}
|
|
1846
1868
|
claimPersonnel(t) {
|
|
1847
1869
|
const need = this.operationSpecs.get(t.kind)?.personnelSpecification;
|
|
1848
1870
|
if (!need?.length)
|
|
@@ -1860,7 +1882,16 @@ export class FlowEngine {
|
|
|
1860
1882
|
다른 등급을 상속한다. "생산직 2명" 요구를 "용접 자격자" 가 만족해야 한다.
|
|
1861
1883
|
유효기간 밖의 등급은 닫힘에서 빠진다(만료된 자격으로 배정되지 않는다). */
|
|
1862
1884
|
(req.personnelClass === undefined ||
|
|
1863
|
-
classClosure(p.personnelClassIds, this.classDefs.personnel, this.now()).has(req.personnelClass))
|
|
1885
|
+
classClosure(p.personnelClassIds, this.classDefs.personnel, this.now()).has(req.personnelClass)) &&
|
|
1886
|
+
/*
|
|
1887
|
+
* **요구된 시험을 만족하나** — 등급이 요구하고(`testSpecificationIds`) 사람이 결과를 든다
|
|
1888
|
+
* (`testResults`). 만료·불합격이면 그 자격은 성립하지 않는다.
|
|
1889
|
+
*
|
|
1890
|
+
* 결과가 **아예 없으면 막지 않는다**: 없는 것으로 막으면 자격자가 전부 사라져 라인이 영구히
|
|
1891
|
+
* 굶고, 원본이 결과를 주지 않는 현장에서는 그 제약이 선언되지 않은 것이다("선언한 것만
|
|
1892
|
+
* 제약이 된다" — 이 커널의 규율).
|
|
1893
|
+
*/
|
|
1894
|
+
this.qualifiedByTests(p));
|
|
1864
1895
|
if (avail.length < want)
|
|
1865
1896
|
return null; // 한 등급이라도 모자라면 시작하지 않는다
|
|
1866
1897
|
for (let i = 0; i < want; i++)
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -99,6 +99,7 @@ __export(index_exports, {
|
|
|
99
99
|
lgtinClass: () => lgtinClass,
|
|
100
100
|
locationStatusOf: () => locationStatusOf,
|
|
101
101
|
mapRecord: () => mapRecord,
|
|
102
|
+
meetsTests: () => meetsTests,
|
|
102
103
|
minuteOfDayAt: () => minuteOfDayAt,
|
|
103
104
|
monteCarloForecast: () => monteCarloForecast,
|
|
104
105
|
objectEvent: () => objectEvent,
|
|
@@ -123,6 +124,7 @@ __export(index_exports, {
|
|
|
123
124
|
ssccUri: () => ssccUri,
|
|
124
125
|
stateFieldsOf: () => stateFieldsOf,
|
|
125
126
|
subLotIdOf: () => subLotIdOf,
|
|
127
|
+
testPassedAt: () => testPassedAt,
|
|
126
128
|
transactionEvent: () => transactionEvent,
|
|
127
129
|
transformationEvent: () => transformationEvent,
|
|
128
130
|
validateDomainDefinition: () => validateDomainDefinition,
|
|
@@ -218,6 +220,21 @@ function hierarchyOf(s) {
|
|
|
218
220
|
}
|
|
219
221
|
};
|
|
220
222
|
}
|
|
223
|
+
function testPassedAt(r, at) {
|
|
224
|
+
if (r.result !== "pass") return false;
|
|
225
|
+
if (!r.expiresAt || !at) return r.result === "pass";
|
|
226
|
+
return Date.parse(at) <= Date.parse(r.expiresAt);
|
|
227
|
+
}
|
|
228
|
+
function meetsTests(required, results, at) {
|
|
229
|
+
if (!required.length) return true;
|
|
230
|
+
const bySpec = new Map((results ?? []).map((r) => [r.specId, r]));
|
|
231
|
+
for (const specId of required) {
|
|
232
|
+
const r = bySpec.get(specId);
|
|
233
|
+
if (!r) continue;
|
|
234
|
+
if (!testPassedAt(r, at)) return false;
|
|
235
|
+
}
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
221
238
|
function effectivityAt(p, at) {
|
|
222
239
|
if (!p || !at) return void 0;
|
|
223
240
|
const atMs = Date.parse(at);
|
|
@@ -2394,10 +2411,34 @@ var FlowEngine = class {
|
|
|
2394
2411
|
return { id: n.id, type: n.type, capacity: n.capacity, parallelism: n.parallelism, occupancy: 0, status: "idle", parentId: n.parentId };
|
|
2395
2412
|
}
|
|
2396
2413
|
buildPerson(p) {
|
|
2397
|
-
return {
|
|
2414
|
+
return {
|
|
2415
|
+
id: p.id,
|
|
2416
|
+
personnelClassIds: p.personnelClassIds,
|
|
2417
|
+
status: "idle",
|
|
2418
|
+
taskId: null,
|
|
2419
|
+
window: p.window,
|
|
2420
|
+
...p.workCalendar ? { workCalendar: p.workCalendar } : {},
|
|
2421
|
+
...p.homeLocation ? { location: p.homeLocation } : {},
|
|
2422
|
+
...p.properties ? { properties: p.properties } : {},
|
|
2423
|
+
...p.testResults ? { testResults: p.testResults } : {},
|
|
2424
|
+
...p.testSpecificationIds ? { testSpecificationIds: p.testSpecificationIds } : {},
|
|
2425
|
+
...p.testResults ? { testResults: p.testResults } : {},
|
|
2426
|
+
...effectiveOnly(p)
|
|
2427
|
+
};
|
|
2398
2428
|
}
|
|
2399
2429
|
buildAsset(a) {
|
|
2400
|
-
return {
|
|
2430
|
+
return {
|
|
2431
|
+
id: a.id,
|
|
2432
|
+
assetClassIds: a.assetClassIds,
|
|
2433
|
+
location: a.homeLocation,
|
|
2434
|
+
status: "idle",
|
|
2435
|
+
taskId: null,
|
|
2436
|
+
...a.properties ? { properties: a.properties } : {},
|
|
2437
|
+
...a.testResults ? { testResults: a.testResults } : {},
|
|
2438
|
+
...a.testSpecificationIds ? { testSpecificationIds: a.testSpecificationIds } : {},
|
|
2439
|
+
...a.testResults ? { testResults: a.testResults } : {},
|
|
2440
|
+
...effectiveOnly(a)
|
|
2441
|
+
};
|
|
2401
2442
|
}
|
|
2402
2443
|
/**
|
|
2403
2444
|
* 이 커널이 **관측으로 구동된다**고 선언한다 — 미러가 첫 이벤트를 받기 전에도 그렇다.
|
|
@@ -3799,6 +3840,23 @@ var FlowEngine = class {
|
|
|
3799
3840
|
});
|
|
3800
3841
|
}
|
|
3801
3842
|
}
|
|
3843
|
+
/**
|
|
3844
|
+
* 이 사람이 **속한 등급들이 요구하는 시험**을 만족하나.
|
|
3845
|
+
*
|
|
3846
|
+
* 요구는 등급이 말하고(`ResourceClassDef.testSpecificationIds`) 기록은 사람이 든다. 상속을 타고 닫은
|
|
3847
|
+
* 등급 전부의 요구를 모아 본다 — 상위 등급이 요구하는 시험도 자격의 조건이다.
|
|
3848
|
+
*
|
|
3849
|
+
* 판정 규칙은 계약이 소유한다(`meetsTests`) — 여기서 다시 적으면 규칙이 두 곳이 되고, 한쪽이 만료를
|
|
3850
|
+
* 조용히 통과시킨다.
|
|
3851
|
+
*/
|
|
3852
|
+
qualifiedByTests(p) {
|
|
3853
|
+
const defs = this.classDefs.personnel;
|
|
3854
|
+
if (!defs?.length) return true;
|
|
3855
|
+
const closure = classClosure(p.personnelClassIds, defs, this.now());
|
|
3856
|
+
const required = [];
|
|
3857
|
+
for (const d of defs) if (closure.has(d.id)) required.push(...d.testSpecificationIds ?? []);
|
|
3858
|
+
return meetsTests(required, p.testResults, this.now());
|
|
3859
|
+
}
|
|
3802
3860
|
claimPersonnel(t) {
|
|
3803
3861
|
const need = this.operationSpecs.get(t.kind)?.personnelSpecification;
|
|
3804
3862
|
if (!need?.length) return [];
|
|
@@ -3811,7 +3869,15 @@ var FlowEngine = class {
|
|
|
3811
3869
|
/* 자격은 **상속을 타고 닫아** 판정한다 — 사람은 여러 등급에 속할 수 있고(표준), 등급은
|
|
3812
3870
|
다른 등급을 상속한다. "생산직 2명" 요구를 "용접 자격자" 가 만족해야 한다.
|
|
3813
3871
|
유효기간 밖의 등급은 닫힘에서 빠진다(만료된 자격으로 배정되지 않는다). */
|
|
3814
|
-
(req.personnelClass === void 0 || classClosure(p.personnelClassIds, this.classDefs.personnel, this.now()).has(req.personnelClass))
|
|
3872
|
+
(req.personnelClass === void 0 || classClosure(p.personnelClassIds, this.classDefs.personnel, this.now()).has(req.personnelClass)) && /*
|
|
3873
|
+
* **요구된 시험을 만족하나** — 등급이 요구하고(`testSpecificationIds`) 사람이 결과를 든다
|
|
3874
|
+
* (`testResults`). 만료·불합격이면 그 자격은 성립하지 않는다.
|
|
3875
|
+
*
|
|
3876
|
+
* 결과가 **아예 없으면 막지 않는다**: 없는 것으로 막으면 자격자가 전부 사라져 라인이 영구히
|
|
3877
|
+
* 굶고, 원본이 결과를 주지 않는 현장에서는 그 제약이 선언되지 않은 것이다("선언한 것만
|
|
3878
|
+
* 제약이 된다" — 이 커널의 규율).
|
|
3879
|
+
*/
|
|
3880
|
+
this.qualifiedByTests(p)
|
|
3815
3881
|
);
|
|
3816
3882
|
if (avail.length < want) return null;
|
|
3817
3883
|
for (let i = 0; i < want; i++) picked.push(avail[i].id);
|
|
@@ -5119,6 +5185,7 @@ function retiredVocabularyIn(line) {
|
|
|
5119
5185
|
lgtinClass,
|
|
5120
5186
|
locationStatusOf,
|
|
5121
5187
|
mapRecord,
|
|
5188
|
+
meetsTests,
|
|
5122
5189
|
minuteOfDayAt,
|
|
5123
5190
|
monteCarloForecast,
|
|
5124
5191
|
objectEvent,
|
|
@@ -5143,6 +5210,7 @@ function retiredVocabularyIn(line) {
|
|
|
5143
5210
|
ssccUri,
|
|
5144
5211
|
stateFieldsOf,
|
|
5145
5212
|
subLotIdOf,
|
|
5213
|
+
testPassedAt,
|
|
5146
5214
|
transactionEvent,
|
|
5147
5215
|
transformationEvent,
|
|
5148
5216
|
validateDomainDefinition,
|
package/package.json
CHANGED